← 返回 Roadmap
Python asyncio 基礎 · 深入版

Python asyncio:從生活直覺一路接到 Event Loop 與 OS 實作

每個概念都使用同一條學習路徑:生活比喻、工程術語橋接、high-level 定義、 底層實作與實際使用。並補上 runtime 的正式定位,以及 asyncio event loop、Selector/Proactor、uvloop、libuv 與 FastAPI workers 的關係。

Coroutine 可暫停與恢復的工作內容
Task event loop 排程 coroutine 的容器
Thread 作業系統的執行路徑
Process 具有獨立記憶體的程式執行個體

1. 全篇閱讀架構:先聽懂,再接上工程術語

只用比喻會產生一個問題:你覺得自己「好像懂了」,但看到文件裡的 TaskFuture、selector、callback、event-loop implementation 時仍然接不上。反過來,只堆工程名詞又會讓初學者不知道每個元件到底在解決什麼。

因此本教材統一採用五層說明法。 每個重要概念都會從生活直覺開始,最後落到正式定義、底層機制與實際程式碼。
① 生活直覺先回答「為什麼需要它」與「它大概像什麼」。
② 術語橋接把比喻中的每個角色一一換成工程名詞。
③ High-level 定義用正式但可理解的句子說清楚責任與邊界。
④ 底層做法說明排程、資料結構、OS 機制與不同實作。
⑤ 實際使用程式碼、選擇規則、效能限制與常見錯誤。

1先建立四個概念所在的層級

Coroutine、Task、Thread、Process 不是同一層的四個替代選項。 它們分別回答不同問題:

Coroutine這份工作要做什麼?可以在哪裡暫停?
Taskevent loop 要如何追蹤、排程、取消這份 coroutine?
Thread哪一條 OS 執行路徑正在真正執行指令?
Process程式在哪一個獨立記憶體與 Python interpreter 中執行?

2從比喻正式轉成工程模型

工程術語橋接

「可暫停的工作流程」正式叫 coroutine; 「event loop 可以管理的一份進行中工作」叫 Task; 「實際執行機器指令的 OS execution context」叫 Thread; 「具有獨立虛擬位址空間與資源的程式執行個體」叫 Process

3先看整體資料流

先不要把 event loop 想成 Thread。 Event loop 是一套長時間重複執行的協調與排程機制;它通常跑在某一條 Thread 上, 但「排程器」和「承載排程器的執行緒」是兩個不同概念。下一章會完整拆解。

2. Runtime 到底是什麼?它不是單一元件,而是「執行中的支援環境」

① 生活直覺劇本是程式碼;真正讓演出發生的舞台、工作人員與規則是 runtime。
② 術語橋接語法=language;執行引擎與狀態=runtime;實際承載單位=Process/Thread。
③ High-level 定義程式執行期間,負責解讀、管理與支援程式的一組軟體機制與狀態。
④ 分層定位CPython runtime、asyncio runtime、ASGI server runtime、application runtime。
⑤ 實際閱讀看到 runtime 時,先問「是哪一層的 runtime?」

1生活直覺:程式碼像劇本,runtime 像整套演出系統

一份劇本只描述角色要說什麼、何時進場,但劇本自己不會演出。 真正演出時還需要舞台、演員、燈光、道具、場務、時間控制與錯誤處理。

把它換成程式:

劇場比喻工程術語作用
劇本規則Python language syntax/semantics定義 defasync defawait 等語法代表什麼
整套演出系統Runtime讓程式在執行期間真的能建立物件、呼叫函式、處理例外與管理資源
一間劇場Process提供獨立記憶體與 OS 資源
正在工作的演員/工作人員Thread實際執行機器指令與 Python code
非同步場務與派場系統asyncio runtime/event loop machinery管理 Tasks、Futures、timers 與 I/O 通知

從比喻接到正式工程定義

Runtime(執行期環境)是個統稱,指程式「正在執行時」, 為它提供執行能力、資料狀態、資源管理與服務的一組軟體機制。 它不是像 TaskThread 一樣必然對應到單一物件。

2先分清楚:language、implementation、runtime

Python language

定義語法與語意,例如 await 能出現在哪裡、function call 應有什麼行為。它是一套規格與抽象規則。

Python implementation

實作這套語言的軟體,例如 CPython、PyPy。同一份 Python 語法可以由不同 implementation 執行。

Runtime

implementation 真正啟動並執行程式後,存在於執行期間的引擎、服務與狀態。使用 CPython 時,常簡稱為 CPython runtime 或 Python runtime。

Runtime state

執行期間保存的狀態,例如已建立的 Python objects、modules、interpreter state、Thread state、exception state 與目前 call frames。

一句正式定義: Python 程式碼描述要做什麼;CPython implementation 提供執行程式; CPython runtime 則是它執行期間實際運作的 interpreter、物件系統、記憶體管理與相關狀態。

Compile time 與 runtime 的差別

# 這段程式可以先被讀取、解析並編譯成 bytecode
def divide(a, b):
    return a / b
# 直到程式真的執行到這一行,才在 runtime 發生 ZeroDivisionError
divide(10, 0)

Compile time 關注把原始碼解析、檢查並轉成可執行形式; runtime 關注程式真正跑起來後發生的 function calls、物件建立、I/O、例外、排程與資源管理。

3CPython runtime 在這張架構圖的哪裡?

它不是夾在 Thread 與 event loop 中間的一個盒子,而是遍布整個 Python Process 的執行環境。 Process 提供 OS 隔離;CPython runtime 在該 Process 裡管理 Python 世界; Threads 再進入某個 interpreter context 執行 Python code。

CPython runtime 主要提供的能力包括:

  • 執行 Python bytecode 與 function calls。
  • 建立、保存與操作 Python objects。
  • 管理 call frames、tracebacks 與 exceptions。
  • 管理記憶體,例如 reference counting 與 garbage collection。
  • 維護 modules、imports、builtins 與 interpreter state。
  • 協調 Thread 進入 Python interpreter 的執行狀態;具體行為取決於 CPython build 與版本。
Process 不等於 runtime。 Process 是 OS 建立的隔離容器;CPython runtime 是跑在該 Process 裡的語言執行環境。 一個 Process 還可能承載 native libraries 與其他 Threads。

4為什麼有時又聽到 asyncio runtime?

「asyncio runtime」通常是工程師使用的方便總稱,不是一個獨立的 OS 元件。 它指的是 asyncio 程式執行時一起合作的機制,例如:

Event loop

執行 ready callbacks、處理 timers,並連接 OS I/O。

Task

驅動 coroutine,保存結果、錯誤與取消狀態。

Future

代表尚未完成、未來會出現的結果。

Executor bridge

把 blocking 或 CPU 工作交給 Thread/Process pool。

這些東西仍然是由 CPython runtime 執行的 Python/native objects,event loop 也仍然跑在某一條 Thread 上。 因此兩者不是競爭關係:

CPython runtime 是更底層、範圍更大的語言執行環境; asyncio runtime 是建立在它之上的非同步執行與協調機制。

asyncio、event loop 與 runtime 的關係

asyncio package提供 API 與類別:run()、Task、Future、Queue、Lock、streams。
asyncio runtime machinery這些元件在程式執行期間形成的協作系統。
event loop其中負責排程 callbacks/Tasks、timers 與 I/O 的核心元件。
loop implementation內建 Selector/Proactor loop 或 uvloop。
OS facilitiesepollkqueue、IOCP、Threads、sockets。
所以 event loop 只是 asyncio runtime 的核心之一,不等於整個 runtime。 Task、Future、Queue、Lock、executor integration 等也屬於非同步執行環境的一部分。

5ASGI server runtime 又是什麼?

當你執行 Uvicorn 時,還會多一層 server runtime。它負責把網路世界與 FastAPI application 接起來:

  1. 建立 server Process、event loop 與 listening socket。
  2. 接受 TCP connections,解析 HTTP/WebSocket protocol。
  3. 依照 ASGI 規格建立 scopereceivesend
  4. 把 FastAPI application 作為 async callable 呼叫並等待。
  5. 處理 startup/shutdown lifespan、signals、connections 與 worker lifecycle。

圖中的層不是單純「上層只能呼叫下一層」的硬切割,而是責任分工。 例如 Uvicorn 使用 event loop,event loop 呼叫 OS I/O,FastAPI coroutine 則由 Task 推進。

用一條啟動命令走完整個 runtime 鏈

uvicorn main:app --loop uvloop --workers 4
  1. OS 建立四個 worker Processes。
  2. 每個 Process 啟動一份 CPython runtime/interpreter state。
  3. CPython 執行 Uvicorn server code。
  4. Uvicorn 在每個 worker 選擇 uvloop 作為 event-loop implementation。
  5. asyncio 的 Task/Future/awaitable 模型建立在該 loop 上運作。
  6. Uvicorn 將 HTTP/WebSocket events 轉成 ASGI messages。
  7. FastAPI endpoint coroutine 被 request Task 推進。
  8. uvloop/libuv 再透過 OS I/O 機制等待 sockets 與 timers。
runtime 的角色是「讓各層真的運作起來」。它不是另一個排程單位;Task、Thread、Process 仍然各有自己的正式角色。

6同一句 runtime,在不同上下文可能指不同東西

常見說法通常指什麼在本教材的位置
Python runtime正在執行 Python 的 interpreter、object system、memory 與 runtime stateProcess 內,支撐所有 Python 程式
CPython runtimePython runtime 的 CPython 實作Python Process 內的核心執行環境
asyncio runtime(方便總稱:loop、Tasks、Futures 等)event loop、Tasks、Futures、callbacks、timers、executors 的協作機制CPython runtime 之上、application 之下
ASGI server runtimeUvicorn 等 server 在執行期間負責 connection、protocol、ASGI calling 與 lifecycle 的系統網路與 FastAPI application 之間
Application runtime應用程式正在運作時的 dependencies、pools、caches、background Tasks 與 stateFastAPI application 自己的執行中環境
Runtime error程式實際執行到某處才發生的錯誤描述錯誤發生時機,不是一個軟體元件
看到 runtime 不要立刻畫成一個盒子。 先看上下文並問:「誰的 runtime?它包含哪些服務與狀態?」若作者只寫 runtime 而沒有說明,這個詞本身可能是模糊的。

7最終定位:它和 Process、Thread、Event Loop 的關係

概念它回答的問題是否是單一具體元件?
Runtime程式執行時,靠哪些引擎、服務與狀態才能運作?通常不是,是一個範圍依上下文而變的統稱
Process程式在哪個 OS 隔離容器與記憶體空間裡?是,OS 管理的執行與資源單位
Thread哪條 execution path 正在實際執行指令?是,OS Thread
Event Loop非同步工作、timers 與 I/O 通知如何被協調?是,一個具體 loop object/implementation
Task哪一份 coroutine 工作正在被追蹤與排程?是,一個 asyncio Task object
一句話收尾:Runtime 是「執行中的環境」;Process 是它所在的 OS 容器;Thread 是實際執行路徑;event loop 是非同步協調核心;Task 是 event loop 管理的工作單位。

3. Event Loop 完整拆解:它是什麼、做什麼、有哪些實作?

① 生活直覺總務台不替每個人完成工作,但持續接收通知與派發下一步。
② 術語橋接通知=event;待辦=callback/Task;等待設備=OS I/O facility。
③ High-level 定義單一 Thread 中長時間運作的非同步協調核心。
④ 底層做法ready queue、timer queue、selector/proactor、thread-pool fallback。
⑤ 實際選擇asyncio built-in loop、uvloop、Uvicorn --loop。

1生活直覺:event loop 像學校總務台

想像總務台同時處理很多事情:冷氣報修、包裹到校、教室鑰匙申請、影印完成通知。 總務人員不會在冷氣維修員抵達前一直站在門口等;他會先記下這件事,去處理其他目前能做的工作。 當電話、廣播或系統通知「維修員到了」,再接續原本的流程。

總務台反覆做的事是:

  1. 查看目前有哪些事情已經可以處理。
  2. 選一件事情,執行它的下一小段。
  3. 若需要等待外部結果,就登記「結果到了再通知我」。
  4. 繼續處理其他 ready 的事情。
  5. 沒有事情可做時,睡到下一個通知或計時器到期。

從比喻接到工程術語

總務台的「待處理清單」對應 ready queue; 一件進行中的工作對應 Task; 「兩秒後再處理」對應 timer/scheduled callback; 「網路資料到了再通知」對應 OS I/O event; 負責反覆接通知與派發工作的核心,就是 event loop

2High-level 工程定義

Event loop 是一個長時間運作的協調器。 它在某一條 OS Thread 中反覆等待事件、執行已就緒的 callbacks/Tasks、處理 timers, 並把網路 I/O、subprocess 或 thread-pool 完成通知轉回 Python 工作。

它的主要用途可以拆成四類:

排程可執行工作

執行 ready callbacks,並讓 Task 推進其 coroutine。

管理時間

處理 call_later()asyncio.sleep()、timeouts 等 timer。

協調 I/O

向 OS 登記 socket/pipe 等 I/O,收到 ready 或 completion event 後恢復工作。

橋接其他執行環境

接收 thread pool、process pool、DNS、file I/O 或 subprocess 的完成結果。

Event loop 不負責什麼?

  • 它不會讓純 Python CPU 計算自動變成多核心平行。
  • 它不會把任意 blocking function 自動變成 non-blocking。
  • 它不是 Task,也不是 Thread;它是執行於 Thread 上的排程與 I/O 協調機制。
  • 它不能讓資料庫或第三方 API 本身更早完成,只能更有效率地管理等待。

3一輪 event loop 在做什麼?

以下是適合初學者使用的簡化模型:

  1. 搬移到期 timers:把已到時間的 scheduled callbacks 放入 ready queue。
  2. 執行 ready work:逐一執行 ready callbacks,或讓 Task 推進 coroutine 一小段。
  3. Task 遇到等待:若 coroutine await 的 Future 尚未完成,Task 暫停,並在 Future 上登記喚醒 callback。
  4. 計算可睡多久:依最接近的 timer、是否已有 ready work 等條件決定 I/O poll timeout。
  5. 等待 OS event:讓 selector/proactor/libuv 等底層機制等待 I/O 或 completion。
  6. 接收通知:將完成的 I/O、timer、thread-pool 結果轉成 callbacks,放回 ready queue。
  7. 下一輪:只要還有活動中的工作,loop 就繼續重複。

工程上不同 event-loop implementation 的輪次細節並不完全相同;上圖是共通心智模型, 不是要求 application developer 依賴的固定內部 API。

4I/O 等待有哪些底層做法?

「等待網路」不是只有一種實作。常見模型可以先分成 readiness 與 completion 兩大類, 再加上無法直接非同步化時使用的 thread-pool fallback。

模型直覺問題工程做法代表機制
Readiness/Selector 「哪一個 socket 現在已經可以讀或寫?」 Loop 登記 file descriptors;OS 回報 readiness;loop 再執行實際 read/write。 Linux epoll、macOS/BSD kqueue、poll、select
Completion/Proactor 「我提交的 read/write 操作哪一個已經完成?」 先提交非同步操作;OS 完成後回報結果與完成通知。 Windows IOCP/overlapped I/O
Thread-pool fallback 「這個 API 本身只能阻塞等待,怎麼不擋住 loop?」 把 blocking operation 交給 worker Thread,完成後再通知 event-loop Thread。 同步 file I/O、部分 DNS、asyncio.to_thread()
Selector 與 Proactor 是兩種 I/O notification model,不是 coroutine 與 Task 的替代品。 Coroutine/Task 是 Python async 工作模型;Selector/Proactor 是 event loop 底下如何取得 OS I/O 通知的策略。

5那 asyncio 到底是哪一種?

asyncio 不是單一演算法,也不只是一個 event loop class。 它是 Python 標準函式庫中的非同步程式設計套件,包含:

  • 高階使用方式:asyncio.run()、TaskGroup、Queue、Lock、streams。
  • 工作抽象:Task、Future、callback、transport/protocol。
  • event-loop API 與內建 implementations。
  • 與 thread/process executor、subprocess、networking 的整合。
Python syntaxasync defawait:語言層級的 coroutine 語法
asyncio high-level APIrun、TaskGroup、Queue、streams:application 常用介面
event-loop contract排 callbacks、建立 Task/Future、network I/O、timer、executor
loop implementationSelectorEventLoop、ProactorEventLoop、uvloop 或其他 custom loop
OS mechanismepoll、kqueue、IOCP、thread pool 等

CPython 內建 asyncio 一般選哪一種?

平台/情境常見內建 loop底層觀念
Linux SelectorEventLoop 通常透過 selectors.DefaultSelector 使用 epoll
現代 macOS/BSD SelectorEventLoop 通常由 DefaultSelector 選擇 kqueue
Windows 一般 CPython ProactorEventLoop(自 Python 3.8 起為預設) completion-based,使用 Windows overlapped I/O/IOCP
Windows 特殊相容需求 SelectorEventLoop readiness-based,但功能與 socket 數量有不同限制

「asyncio 選哪一種」必須連同 Python 版本、作業系統、server 啟動模式與 custom loop 設定一起回答, 不能只說 asyncio 永遠等於 epoll。

6uvloop 是另一種嗎?

uvloop 是另一個 asyncio-compatible event-loop implementation。 它不是另一套 async/await 語法,也不是額外的 Thread; 它用 Cython 實作 asyncio event-loop contract,底層使用 libuv。

因此同一份 application code 通常不必改寫:

@app.get("/users/{user_id}")
async def get_user(user_id: str):
    return await repository.get_user(user_id)

上層仍使用 asyncio 的 Task、Future 與 awaitable 模型,只是下方換了 loop implementation:

libuv 又是什麼?

libuv 是跨平台 asynchronous I/O library。它提供 event loop、network I/O、timer、process、DNS、file-system 等能力,並在各平台選用合適機制:例如 Linux epoll、macOS/BSD kqueue、Windows IOCP。 對某些無法直接以 non-blocking OS API 處理的操作,libuv 會使用自己的 thread pool。

所以 uvloop 仍然是同一種高階 asyncio 模型,但底層工程實作不同。 可以說它是「另一個 event-loop implementation」,不能說它是「另一種 coroutine」或「多一條 Thread」。

Uvicorn 如何選擇?

# 預設:uvloop 已安裝且平台支援時優先使用,否則退回 asyncio
uvicorn main:app --loop auto

# 明確使用 CPython 內建 asyncio implementation
uvicorn main:app --loop asyncio

# 明確要求 uvloop
uvicorn main:app --loop uvloop

Uvicorn 目前的 --loop auto 會優先選 uvloop,若不可用則使用 Python 內建 asyncio loop。 uvloop 不支援 Windows 或 PyPy;Uvicorn 在 Windows 的具體 built-in loop 選擇還會受到單程序、reload、 multi-worker 模式影響。

uvloop 為什麼可能更快?

它主要降低 event-loop 自己的管理成本:I/O polling、callback dispatch、timer、socket handling、 Future/Task 喚醒等。如果 workload 有大量短小 network events,這些微小成本累積後可能很可觀。

瓶頸uvloop 能否直接改善?原因
大量 socket events、callbacks、timers可能屬於 event-loop implementation 的主要工作
資料庫 query 本身很慢不能時間花在 DB server
第三方 API 回應兩秒不能loop 只能管理等待,不能縮短遠端處理時間
純 Python CPU-heavy 迴圈不能仍會占住 event-loop Thread
requests.get()time.sleep()不能blocking code 根本沒有把控制權還給 loop
實務順序:先消除 blocking code、設定 connection pools/timeouts/concurrency limits, 再以自己的 workload benchmark built-in loop 與 uvloop。不要把換 uvloop 當成架構問題的替代療法。

4. FastAPI Worker:一個 Process、主要 Thread、Event Loop 怎麼接起來?

① 生活直覺一間分店、一位主要櫃台人員、訂單系統與支援人員。
② 術語橋接分店=Process;櫃台=Main Thread;派單系統=Event Loop。
③ High-level 架構每個 worker 有自己的 loop、記憶體與 connection pools。
④ 底層擴充Thread pool 處理 blocking I/O;多 Process 利用多核心。
⑤ 部署選項workers、loop implementation、資源上限與觀測。

1生活直覺與正式術語

把一個 Uvicorn worker 想成一間獨立分店。分店內有一位主要櫃台人員,使用訂單系統協調很多顧客; 某些舊設備只能讓人站在旁邊等,這時才叫支援人員處理。客流再大時,不是讓同一位櫃台人員長出四雙手, 而是多開幾間分店。

工程術語橋接

分店=worker Process;主要櫃台人員=Main Thread; 訂單系統=event loop;訂單=Task; 支援人員=thread-pool Threads;多開分店=multiple worker processes

2High-level 架構

精確說法: 一個 worker 通常有一條主要 event-loop Thread,但 Process 並沒有「只能一條 Thread」的限制。 正常 async network I/O 不需要每個 request 一條 Thread;blocking 工作才可能使用額外 thread pool。

3為什麼一條主要 Thread 可以管理很多 request?

因為 request 大部分時間常在等 DB、Redis、第三方 API 或 client socket。 Event loop 會在一個 Task 等待時執行其他 ready Task;等待本身交給 OS I/O 機制管理。

@app.get("/user")
async def get_user():
    # 等待期間目前 Task 暫停,event loop 可推進其他 Tasks
    return await repository.get_user()

這是 concurrency,不是同一 Thread 的 CPU parallelism。同一瞬間主要 Thread 仍只執行一段 Python code。

4Thread pool 在哪裡出現?

FastAPI 的普通 def path operation 會由外部 thread pool 執行:

@app.get("/legacy")
def legacy_endpoint():
    return blocking_database.query()

async endpoint 也可以明確 offload blocking I/O:

@app.get("/legacy-async")
async def legacy_async_endpoint():
    return await asyncio.to_thread(blocking_sdk.fetch)
不要誤會:async def 裡直接呼叫普通 blocking utility function, FastAPI 不會自動辨識並移到 thread pool;它會直接卡住 event-loop Thread。

5多核心與多 workers

uvicorn main:app --workers 4 --loop auto

這會建立多個 worker Processes;每個 worker 各自有自己的 Main Thread、event loop、memory、global variables、 DB connection pool 與 HTTP client pool。OS 可以把不同 Processes 排到不同 CPU cores 上。

容量要用乘法算: 4 workers × 每個 DB pool 20 connections,理論上可能形成 80 條 connections。 同樣地,in-memory cache 與 global counters 也會有 4 份,而不是共享一份。

uvloop 放在這個架構的哪裡?

uvloop 只替換每個 worker 裡的 event-loop implementation,不會改變 worker 數量、Thread 數量或 endpoint 語法。

--workers調整 Process 數量與多核心容量
Thread Pool處理無法非同步化的 blocking I/O
--loop選擇每個 worker 內 event-loop implementation
Tasks/Semaphore管理單一 loop 內的 async concurrency
語言與執行狀態層級

5. Coroutine:可以暫停並保留現場的工作流程

閱讀順序: 本章先理解 coroutine 本身;下一章會詳細回答 「為什麼不能直接把 Task 功能放進 coroutine」以及直接 await 和 create_task 的真正差異。
① 生活直覺遊戲存檔:暫停後保留角色位置與背包狀態。
② 術語橋接存檔位置=instruction position;背包=local variables。
③ High-level 定義能 suspend/resume 的函式執行狀態。
④ 底層做法coroutine object、frame state、await protocol。
⑤ 實際使用async def、await、忘記 await 的錯誤。

1生活直覺與術語橋接

普通函式像一段必須一路跑完的關卡;coroutine 像可以存檔的關卡。 它暫停時會保留「走到哪裡」與「身上有哪些資料」,等等待條件完成後再從原處繼續。

工程術語橋接

「關卡設計圖」是 coroutine function;呼叫後產生的「這一次遊戲進度」是 coroutine object;目前執行位置、local variables 與 exception state 構成可恢復的執行狀態。

2High-level 定義

Coroutine 是可暫停與恢復的 computation。 在 Python 中,async def 定義 coroutine function;呼叫它會建立 coroutine object, 但不代表已被 event loop 排程。
async def fetch_user():
    user = await database.get_user()
    return user
coro = fetch_user()
print(coro) # coroutine object,尚未完整執行

3底層關係:誰讓它前進?

Coroutine 本身不是排程器。通常由 Task 包裝並驅動它;Task 讓 coroutine 執行, 直到 coroutine 完成、拋錯,或 await 一個尚未完成的 awaitable。

Event Loop
Task:驅動與追蹤
Coroutine object:保存可恢復的執行狀態

await x 的正式意思不是「一定切換 Task」,而是「等待一個 awaitable 的結果」。 如果結果已經完成,可能立即繼續;如果尚未完成,目前 coroutine 才會 suspend。

4實際使用與錯誤

async def main():
    user = await fetch_user() # 正確:等待並取得結果
async def main():
    user = fetch_user() # 錯誤心智模型:這是 coroutine object
    print(user)
async def 不會讓內部所有程式自動 non-blocking。 time.sleep()、同步 HTTP client、同步 DB driver 或大量 CPU 計算仍會阻塞承載它的 Thread。
語言執行狀態 × asyncio 排程層級

6. Coroutine 與 Task 的關係:為什麼有 Coroutine 之後還需要 Task?

① 生活直覺Coroutine 是可暫停的工作流程;Task 是正式啟動、追蹤與管理這份流程的工作單。
② 工程術語橋接Coroutine=resumable computation;Task=event-loop-managed execution unit。
③ High-level 關係Event loop 排程 Task;Task 推進 coroutine;coroutine 透過 await 表達依賴。
④ 底層機制Task step coroutine;coroutine 暫停並交出 Future;Future 完成後喚醒 Task。
⑤ 設計理由分離「工作流程」與「排程、生命週期、取消、結果與 ownership」。
先講最精確的結論:
Coroutine 保存「這段 async 函式目前執行到哪裡,以及接下來怎麼繼續」; Task 則把某一個 coroutine 變成 event loop 可以獨立排程、追蹤、取消並取得結果的工作單位。

1先把四個容易混在一起的東西拆開

async def fetch_user(user_id: str) -> dict:
    user = await database.get_user(user_id)
    return user
coroutine_object = fetch_user("u-123")
task = asyncio.create_task(coroutine_object)
東西 這個例子中是什麼 正式角色
fetch_user Coroutine function 工作流程的定義;類似普通 function 的「程式碼模板」
fetch_user("u-123") Coroutine object 這一次函式呼叫的可暫停執行狀態
task Task object event loop 可以排程與管理的獨立工作
database.get_user(...) 的底層等待結果 常由 Future/Future-like object 表示 未來才會完成的低階結果
Coroutine function 定義流程; coroutine object 保存某次流程的執行現場; Task 決定這次流程成為哪一個獨立排程與生命週期單位。

2Coroutine 到底保存了什麼?

Coroutine object 不是一個抽象的「工作名稱」,而是某一次函式呼叫的執行現場。 它概念上保存:

  • 目前停在哪個 instruction/source position。
  • 區域變數目前的值。
  • 目前的 coroutine frame。
  • 正在 await 哪個 awaitable。
  • 恢復時要接收正常結果,還是被注入 exception。
  • 最後 return 的值,或未處理的 exception。
import inspect
async def example() -> int:
    value = 10
    await asyncio.sleep(1)
    return value + 1
coro = example()
print(inspect.getcoroutinestate(coro))
# CORO_CREATED:已建立,但尚未開始執行

Coroutine 的典型狀態可以理解成:

這些狀態描述的是「函式執行進度」。但是它沒有完整回答: 何時開始、由誰恢復、是否獨立取消、結果由誰保存、錯誤由誰負責收走。

3Task 補上的不是函式內容,而是執行管理

Task 是 Future-like object。它包住一個 coroutine,並加入一個獨立 async 工作需要的管理能力:

Scheduling

把工作交給目前 event loop,在適當時機推進。

Identity

有獨立 Task identity 與可選名稱,方便追蹤和除錯。

Lifecycle

保存 pending、done、cancelled 等生命週期狀態。

Result channel

保存 coroutine 的 return value 或 exception。

Cancellation boundary

可向這份獨立工作提出取消請求。

Context

建立時複製或指定 contextvars.Context

Callbacks

完成後可安排 done callbacks,供低階整合使用。

Ownership

可以被 TaskGroup 或 application 明確持有與等待。

Coroutine 是 computation state;Task 是 execution control + eventual result。 Task 不會改寫 coroutine 的商業邏輯,它只是負責管理「這一次執行」。

4直接 await coroutine:它通常不會變成新的 Task

import asyncio
async def child() -> int:
    print("child task:", asyncio.current_task().get_name())
    await asyncio.sleep(1)
    return 42
async def parent() -> None:
    print("parent task:", asyncio.current_task().get_name())
    result = await child()
    print(result)
asyncio.run(parent())

概念上的結構不是「parent Task 裡再自動產生 child Task」,而是:

因此 parent()child() 中的 asyncio.current_task() 會看到同一個 Task。 它們只是同一個 Task 裡的不同 coroutine frames。

正式說法

直接 await child() 是 async function composition: child coroutine 加入目前 Task 的 await chain,由目前 Task 一起推進。 它不建立新的 scheduling identity。

5create_task:明確建立新的 scheduling identity

import asyncio
async def child() -> int:
    task = asyncio.current_task()
    print("child task:", task.get_name())
    await asyncio.sleep(1)
    return 42
async def parent() -> None:
    parent_task = asyncio.current_task()
    print("parent task:", parent_task.get_name())
    child_task = asyncio.create_task(
        child(),
        name="child-task",
    )
    print("parent continues before child result")
    result = await child_task
    print(result)
asyncio.run(parent())

現在結構變成:

create_task() 不是讓 coroutine「比較 async」; 它是建立新的排程與生命週期邊界,使 child 可以和 parent 交錯前進。

常見但沒有意義的寫法: 建立 Task 後立即 await,而且中間沒有其他工作需要重疊。
# 多數情況下沒有必要
task = asyncio.create_task(fetch_user())
user = await task
# 更直接
user = await fetch_user()

6兩條時間線:直接 await 與建立 Task 到底差在哪裡?

情況 A:直接 await

async def main():
    result_a = await fetch_a()
    result_b = await fetch_b()
Main Task
進入 A
等待 A
A 完成
進入 B
等待 B
B 完成

A 和 B 都在同一個 Main Task 裡;必須等 A 的 await chain 完成, 程式才會執行到建立 B coroutine 的那一行。

情況 B:建立兩個 Tasks

async def main():
    task_a = asyncio.create_task(fetch_a())
    task_b = asyncio.create_task(fetch_b())
    result_a = await task_a
    result_b = await task_b
Task A
開始 A
等待 A 的 I/O
完成
Task B
等待排程
開始 B
等待 B 的 I/O
完成

A、B 現在是兩個 event loop 可以分別恢復的工作單位。 這才是建立 Task 所帶來的 concurrency。

7底層:Task 怎麼「驅動」Coroutine?

Coroutine 具有類似 generator 的恢復介面。概念上可以被:

  • send(...):送入值並繼續執行。
  • throw(...):在暫停點注入 exception。
  • close():要求 coroutine 清理並結束。

Application code 幾乎不應該直接操作這些方法;Task 會替你處理。 以下是高度簡化、只用來建立心智模型的偽程式碼:

class ConceptualTask:
    def __init__(self, coroutine, loop):
        self.coroutine = coroutine
        self.loop = loop
        self.result_value = None
        self.exception_value = None
        self.done = False
        loop.call_soon(self.step)
    def step(self, value=None, exception=None):
        try:
            if exception is None:
                awaited = self.coroutine.send(value)
            else:
                awaited = self.coroutine.throw(exception)
        except StopIteration as completed:
            self.result_value = completed.value
            self.done = True
        except BaseException as error:
            self.exception_value = error
            self.done = True
        else:
            awaited.add_done_callback(self.wake_up)
    def wake_up(self, completed_future):
        try:
            value = completed_future.result()
        except BaseException as error:
            self.loop.call_soon(self.step, None, error)
        else:
            self.loop.call_soon(self.step, value, None)

真正的流程可壓縮成:

這就是為什麼需要 Task:Coroutine 知道自己如何暫停和恢復, 但 Task 負責把它接上 event loop 和 Future completion。

8一個 Task 可以驅動整條巢狀 Coroutine Chain

async def repository_get_user(user_id: str) -> dict:
    return await database.get_user(user_id)
async def service_get_user(user_id: str) -> dict:
    user = await repository_get_user(user_id)
    return normalize(user)
async def endpoint(user_id: str) -> dict:
    return await service_get_user(user_id)

這通常不是三個 Tasks,而是:

Request Task
└── endpoint coroutine
    └── service_get_user coroutine
        └── repository_get_user coroutine
            └── database Future

這是 Task 和 Coroutine 分離最重要的好處之一: 普通 async function composition 不需要為每一層 service/repository 建立新的排程單位。

類比普通同步程式: function call 不等於建立 Thread; 同樣地,await coroutine 不等於建立 Task。

9為什麼不能讓每個 Coroutine 自動具有 Task 功能?

技術上可以設計另一種語言,讓 async function 一呼叫就自動啟動。 Python 沒這樣做,是為了保留以下重要性質。

理由 A:分離工作內容與排程政策

問題 由誰回答?
這份工作有哪些步驟、在哪裡 await、回傳什麼? Coroutine
它是否要和 caller concurrent 執行? 呼叫端透過直接 await 或建立 Task 決定
誰擁有它、何時等待、何時取消? Task/TaskGroup/application lifecycle

這種分離稱為 separation of computation from scheduling policy: 同一個 coroutine function 可以依情境被循序 await,也可以成為 concurrent Task。

理由 B:避免隱藏 concurrency 與副作用

# Python 現在的行為:只建立 coroutine object
operation = charge_credit_card(order)
# 明確決定何時執行
receipt = await operation
# 或明確建立獨立工作
task = asyncio.create_task(operation)

如果 coroutine 一建立就自動成為 Task,那麼僅僅呼叫函式可能已經開始扣款、寫 DB、 發送 HTTP request 或產生 exception。程式的 concurrency 邊界會變得不透明。

理由 C:大部分巢狀 async calls 不需要獨立 Task

Endpoint → Service → Repository 通常是一條相依工作鏈。 每一層自動建立 Task 只會增加物件、排程、取消與除錯成本,卻不產生有用 concurrency。

理由 D:建立 Coroutine 時可能還沒有 running event loop

async def main():
    ...
coro = main()
# 此刻只建立 coroutine object,可能還沒有 running loop
asyncio.run(coro)
# run() 建立 runner/loop,並負責執行入口 awaitable

Task 必須由某個 event loop 執行;asyncio.create_task() 也要求目前 Thread 已有 running loop。Coroutine object 則可以先存在,再交給 runner。

理由 E:Task 是 Future-like 結果容器

Task 繼承 asyncio Future 的大部分 API,並以 wrapped coroutine 的 return/raise 決定自己的 result/exception。這讓它可以被多個地方等待。

task = asyncio.create_task(fetch_user())
consumer_a = await task
consumer_b = await task
assert consumer_a is consumer_b

相反地,同一個 coroutine object 不能執行兩次:

coro = fetch_user()
first = await coro
second = await coro
# RuntimeError: cannot reuse already awaited coroutine

Coroutine 是一次性的執行現場;Task 是一次執行加上一個可重複讀取的完成結果。

理由 F:取消與錯誤需要明確生命週期邊界

直接 await child coroutine 時,child 是 caller Task 的 await chain; 建立 child Task 後,它有自己的 cancellation state 和 exception channel。

理由 G:Structured concurrency 需要「子工作物件」

async with asyncio.TaskGroup() as group:
    user_task = group.create_task(fetch_user())
    config_task = group.create_task(fetch_config())
# 離開區塊時,兩個 Tasks 都已結束
user = user_task.result()
config = config_task.result()

TaskGroup 可以持有子 Tasks、等待它們、在失敗時取消 sibling Tasks, 並建立清楚的 parent/child lifetime。Raw coroutine object 本身不提供完整的 ownership model。

理由 H:同一份 Coroutine 模型可搭配不同 event-loop implementations

Coroutine 是 Python language/runtime 層級的 awaitable; Task 則由 asyncio/event loop implementation 建立與管理。 這讓內建 asyncio loop 和 uvloop 都能提供相容的 Task scheduling。

10Result、Exception、Cancellation 的傳遞差異

直接 await:像普通函式呼叫鏈

async def child():
    raise ValueError("child failed")
async def parent():
    await child()

錯誤沿同一個 Task 的 await chain 往上傳:

child coroutine
    ↓ ValueError
parent coroutine
    ↓ ValueError
parent Task

獨立 Task:錯誤先存入 Task

async def parent():
    task = asyncio.create_task(child())
    # child 的錯誤先讓 child Task 進入 done 狀態
    await asyncio.sleep(1)
    # 讀取 child Task 結果時,錯誤重新拋出
    await task

如果沒有任何人 await Task 或讀取 task.exception(), 可能出現 Task exception was never retrieved

Cancellation

task = asyncio.create_task(worker())
task.cancel()
try:
    await task
except asyncio.CancelledError:
    print("worker was cancelled")

task.cancel() 會安排在 wrapped coroutine 的下一個適當執行機會注入 CancelledError。Coroutine 可以在 finally 清理資源, 通常應讓取消繼續傳播。

11Task 自己 await Task,會發生什麼?

async def parent():
    child_task = asyncio.create_task(child())
    result = await child_task
    return result

Parent Task await Child Task 時:

  1. Parent Task 暫停。
  2. Child Task 繼續由 event loop 獨立排程。
  3. Child Task 完成後,自己的 Future-like 狀態變成 done。
  4. 完成 callback 讓 Parent Task 重新進入 ready queue。
  5. Parent Task 恢復,取得 Child Task 保存的 result 或 exception。

12Task 的建立時機:不是保證下一行之前一定沒執行

一般心智模型可以說 create_task() 把 coroutine 排定為「很快執行」。 傳統預設通常會在目前工作下一次把控制權交回 event loop 後開始。

但較新的 Python 支援 eager task execution/eager_start: 在啟用時,Task 可能在建立當下就同步推進 coroutine,直到第一次真正 blocking。 因此 production code 不應依賴「create_task 後,child 絕對還沒開始」這種假設。

task = asyncio.create_task(operation())
# 不要把正確性建立在 operation 此刻一定尚未開始
update_shared_state()

13什麼時候直接 await?什麼時候建立 Task?

情境 建議 原因
下一步需要這個結果 await coroutine() 這是一條正常相依呼叫鏈
Service 呼叫 Repository 直接 await 通常不需要新的 scheduling identity
兩個獨立 I/O 可以重疊 TaskGroup.create_task() 建立受管理的 concurrency
需要稍後取消或讀取某工作結果 建立並保存 Task reference 需要獨立生命週期控制
背景工作屬於 application lifecycle 使用明確 supervisor/TaskGroup/framework lifecycle 確保 exception、shutdown 和 cleanup 有 owner
建立 Task 後馬上 await,無其他重疊工作 通常直接 await Task boundary 沒有產生價值

快速決策流程

問題 1:目前工作是否必須先拿到結果才能繼續? 是 → 直接 await
問題 2:它是否可以和目前工作或其他工作重疊進行? 是 → 考慮建立 Task。
問題 3:誰負責等待、取消、處理錯誤與 shutdown? 答不出來 → 先不要建立裸 Task;使用 TaskGroup 或明確 lifecycle owner。
問題 4:其中一個子工作失敗,其他工作是否應停止? 是 → 通常使用 TaskGroup 的 structured concurrency。

14常見誤解修正

誤解 正確理解
Coroutine 就是 Task Coroutine 是可暫停執行狀態;Task 是管理 coroutine 的 Future-like 排程物件
每次 await 都建立一個 Task 直接 await coroutine 通常仍在目前 Task 的 await chain
create_task 會建立 Thread 它只在目前 event loop 建立新的 asyncio Task
Task 讓 CPU code 平行 Task 提供 cooperative concurrency,不會把 blocking CPU code 變成多核心平行
建立 Task 後不用保存 reference 應有明確 owner;TaskGroup 是較安全的管理方式
Task cancel 會立即強制殺死 coroutine 它是合作式取消,會在適當機會向 coroutine 注入 CancelledError

15最後用一句工程術語收尾

Coroutine separates suspendable computation from execution policy.
Coroutine 表達可暫停的計算;Task 為某一次 coroutine 執行加入 scheduling identity、 Future-compatible completion state、cancellation、context 與 lifecycle ownership。
最值得記住的類比: 普通函式呼叫不等於建立 Thread; 同樣地,直接 await coroutine 不等於建立 Task。 只有當你需要一個新的、可獨立管理的 concurrent 工作時,才建立 Task。

本章官方依據

asyncio 排程層級

7. Task:event loop 管理 coroutine 的工作單位

本章定位: 前一章已拆解 Coroutine 與 Task 為什麼分離;這一章專注在 Task 的 API、生命週期與實務管理。
① 生活直覺Coroutine 是流程表;Task 是已立案、可追蹤的案件。
② 術語橋接案件編號=Task identity;進度=state;撤案=cancel。
③ High-level 定義被 event loop 排程的 coroutine wrapper。
④ 底層做法step coroutine、等待 Future、登記 done callback。
⑤ 實際使用create_task、TaskGroup、result/exception/cancel。

1生活直覺與工程術語

Coroutine 像一份辦案流程;Task 是把某一份流程正式立案後的案件。 立案後系統可以知道案件是否待處理、正在等待、完成、失敗或被取消。

工程術語橋接

「立案」是建立 Task;「案件流程」是 coroutine; 「案件結果」是 Task result;「辦案失敗原因」是 exception; 「撤案請求」是 cancellation。

2High-level 定義

Task 是 asyncio 用來排程、推進並追蹤 coroutine 的 Future-like object。 它把一份 coroutine 變成 event loop 可以獨立管理的工作單位。
task = asyncio.create_task(fetch_user(), name="fetch-user")
user = await task

3底層運作

  1. Task 被建立並排入 event loop 的 ready work。
  2. Event loop 執行 Task;Task 推進 coroutine。
  3. Coroutine await 尚未完成的 Future,Task 暫停。
  4. Task 在該 Future 上登記 wake-up callback。
  5. Future 完成後 callback 讓 Task 重新進入 ready queue。
  6. Coroutine return 時,Task 儲存 result;拋錯時儲存 exception。

4為什麼連續 await 與 create_task 不同?

# 有先後相依:A 完成後才會開始 B
result_a = await fetch_a()
result_b = await fetch_b()
# 兩份工作先各自立案,由 event loop 交錯推進
task_a = asyncio.create_task(fetch_a())
task_b = asyncio.create_task(fetch_b())
result_a = await task_a
result_b = await task_b

5實務管理:優先使用 TaskGroup

async with asyncio.TaskGroup() as group:
    user_task = group.create_task(fetch_user())
    config_task = group.create_task(fetch_config())
user = user_task.result()
config = config_task.result()

TaskGroup 建立明確的生命週期邊界:離開區塊前會等待子 Tasks;其中一個發生未處理錯誤時, 會取消其他 sibling Tasks 並彙整錯誤。這是 structured concurrency。

Task 不是背景 Thread。 多個 Tasks 通常仍共用同一條 event-loop Thread;其中任何 Task 執行 blocking code,其他 Tasks 也會受影響。
作業系統執行層級

8. Thread:Process 內真正執行指令的一條路徑

① 生活直覺同一間廚房裡的多位廚師,共用冰箱與工作台。
② 術語橋接廚房=Process memory;廚師=Thread;個人筆記=stack。
③ High-level 定義OS 可排程的 execution context。
④ 底層做法共享 address space、各自 stack、preemptive scheduling、GIL。
⑤ 實際使用blocking I/O、thread pool、Lock 與 thread safety。

1生活直覺與術語橋接

多條 Threads 像同一間廚房裡的多位廚師:大家能直接拿同一台冰箱裡的材料,溝通很快; 但也可能同時修改同一鍋湯,造成資料競爭。

工程術語橋接

共用廚房是 shared address space/heap;每位廚師自己的工作步驟與暫存資料是 call stack;作業系統臨時切換哪位廚師工作是 preemptive scheduling; 保護共用鍋子的規則是 Lock/synchronization

2High-level 定義

Thread 是 Process 內的 OS execution context。 同 Process 的 Threads 共享大多數記憶體與資源,但各自擁有 stack、register state 與排程狀態。

3與 Task 的核心差異

面向asyncio TaskOS Thread
排程者Event loopOperating system/runtime
切換主要在 await 等合作式讓出可在較難預測的位置被搶佔
建立成本較低較高,需要 OS stack 與排程資源
blocking call會卡住共用 event-loop Thread只直接阻塞該 Thread,但 pool 仍有容量限制
適合規模大量 I/O Tasks有限數量 blocking workers

4GIL 與真正平行

在一般預設 CPython 中,GIL 通常限制同一 interpreter 內同時執行 Python bytecode 的 Threads 數量, 因此純 Python CPU-heavy 工作不會因增加 Threads 而線性加速。Thread 仍適合 blocking I/O, 某些釋放 GIL 的 native libraries 也可能平行執行。

5實際使用

result = await asyncio.to_thread(
blocking_sdk.fetch,
request_id,
)

這不是把 blocking function 變成真正 async API,而是把等待成本移到 thread pool, 讓 event-loop Thread 可以繼續服務其他 Tasks。

Thread pool 不是無限資源。若大量長時間 blocking calls 塞滿 pool,新的工作仍會排隊; 同時還要確認第三方 client 與共享物件是否 thread-safe。
作業系統隔離層級

9. Process:具有獨立虛擬記憶體與執行資源的程式個體

① 生活直覺不同分店:各自有廚房、庫存與員工。
② 術語橋接分店倉庫=address space;物流=IPC/serialization。
③ High-level 定義OS 管理的資源與隔離單位。
④ 底層做法獨立 interpreter、virtual memory、IPC、startup method。
⑤ 實際使用CPU-heavy、worker processes、成本與資料傳輸。

1生活直覺與術語橋接

不同 Processes 像不同分店。每間店有自己的庫存與工作台;一間店修改庫存,不會直接改到另一間。 要交換物品必須透過物流或正式通訊管道。

工程術語橋接

分店自己的倉庫是 virtual address space;自己的 Python 執行環境是 interpreter/CPython runtime state;分店間物流是 IPC; 把 Python object 包裝成可傳輸資料是 serialization/pickling

2High-level 定義

Process 是作業系統的資源隔離與執行單位。 它通常擁有自己的虛擬記憶體、Python interpreter、Threads、file descriptors 與 application state。

3為什麼適合 CPU-heavy?

多個 Processes 可以由 OS 排到不同 CPU cores 上,並各自執行 Python interpreter, 因此可用於純 Python CPU-heavy parallelism。代價是啟動、記憶體與跨 Process 傳輸較昂貴。

from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(number: int) -> int:
    return sum(value * value for value in range(number))
with ProcessPoolExecutor() as pool:
    results = list(pool.map(cpu_heavy, [20_000_000] * 4))

4FastAPI workers 也是 Processes

uvicorn --workers 4 建立的是四個 server Processes,不是四個 Tasks。 它們各自載入 application,也各自擁有 event loop 與 connection pools。

5選擇限制

  • 資料傳輸可能需要 serialization,巨大資料的 IPC 成本可能抵銷平行收益。
  • 每個 Process 都會增加記憶體與連線數。
  • 某個 Process 內的普通 global variable 不會自動與其他 Processes 同步。
  • 影片轉碼、模型推論等工作也可能已有自己的 native threads/GPU,需要避免過度並行。
Process 不是更強的 Task。 大量 HTTP 等待通常使用 Tasks 更省資源;Process 主要用於隔離、多核心、故障邊界或獨立 worker lifecycle。

10. 完整比較表:把五層理解壓縮成一張工程決策表

比較面向 Coroutine Task Thread Process
本質 可暫停的函式執行狀態 包裝並排程 coroutine 的 asyncio 物件 OS 執行路徑 獨立程式執行個體
抽象層級 Python 語言 asyncio runtime 作業系統 作業系統
建立方式 呼叫 async def 函式 asyncio.create_task() / TaskGroup Thread / ThreadPoolExecutor Process / ProcessPoolExecutor
誰排程 本身不排程 Event loop OS scheduler;CPython runtime/GIL 會影響 Python code 的執行 OS
切換模型 合作式 合作式 搶佔式 搶佔式
共享記憶體 直接共享 直接共享 直接共享 預設隔離
建立成本 非常低 非常低 中等
數量規模 可很多,但受記憶體與工作內容限制 通常可管理成千上萬個 I/O 等待工作 通常數十至數百,視工作與系統而定 通常接近 CPU core 數或少量 workers
純 Python CPU 平行 預設 CPython 通常否
I/O concurrency 搭配 Task 非常適合 適合 可以但成本較高
等待點 顯式 await 由內部 coroutine 的 await 決定 不需要顯式等待點 不需要顯式等待點
取消 透過驅動它的 Task task.cancel(),合作式取消 不能安全強制殺掉,通常使用 flag/event 可 terminate/kill,但可能破壞 cleanup
資料競爭 可能跨 await 發生 可能跨 await 發生 可能在不可預期的切換點發生 普通變數不共享;共享機制仍可能競爭
常用同步工具 asyncio.Lock、Semaphore、Event、Queue threading.Lock、Event、Queue multiprocessing Lock、Queue、Pipe、Manager
最適合 描述 async 流程 大量 I/O-bound async 工作 blocking I/O 或同步 SDK CPU-heavy 平行計算

11. 排程與切換方式:從直覺切換到 cooperative/preemptive scheduling

本章的術語橋接

「自己在適當位置把麥克風交出去」叫 cooperative scheduling; 「主持人可以在時間片到時切換講者」叫 preemptive scheduling。 asyncio Tasks 主要採前者,OS Threads/Processes 主要採後者。

Task / Coroutine:合作式切換

正在執行的 Task 必須走到一個尚未完成的 await, 才會把控制權交給 event loop。程式設計者相對容易知道可能在哪些位置切換。

Task A
執行
等待 API
恢復
Task B
尚未執行
A 等待時執行
等待 DB
恢復

Thread / Process:搶佔式切換

作業系統可以在時間片用完、Thread 阻塞或其他排程條件出現時切換。 從 application 角度看,切換點比較難預測。

Thread A
執行
被切走
繼續
Thread B
等待排程
執行
等待排程
因此 asyncio 也會有 race condition,但通常只會在 await 邊界附近讓其他 Task 插入。 Thread race condition 的可能切換位置更廣,推理通常更困難。

12. 程式範例:從需求判斷應使用哪一層

以下每個例子都依固定順序閱讀:先判斷工作是在等待還是在計算, 再決定使用 coroutine/Task 表達流程、Thread 橋接 blocking API,或 Process 取得多核心平行。

需求:同時查詢 100 個遠端 API

Coroutine 負責什麼?
Coroutine 描述「發 request → 等 response → 解析結果」的流程,並用 await 標出等待點。但只有 coroutine object 還不代表 100 個工作已開始 concurrent 執行。
Task 負責什麼?
把 100 份 coroutine 變成可獨立排程的 Tasks。當某個 request 等網路時, event loop 可以執行其他 Task。實務上還要用 Semaphore 限制同時連線數。
Thread 能不能做?
可以。可用 ThreadPoolExecutor 執行同步 HTTP client。好處是容易整合既有 blocking library; 缺點是每條 Thread 成本高於 Task,且共享狀態同步較複雜。
Process 適合嗎?
通常不適合。API request 大多是在等待,不需要為每組工作建立獨立 interpreter 和記憶體空間。 Process 的 IPC 與啟動成本通常沒有帶來相對應收益。

需求:將四支影片做 CPU-heavy 編碼

Coroutine / Task 適合嗎?
如果編碼工作直接在 event loop Thread 中執行,會卡住整個 event loop。Task 不會把 CPU 工作變成平行工作。 可讓 async 程式負責協調,再把計算送到 ProcessPool 或外部 worker。
Process 適合嗎?
適合。每個 Process 可以使用不同 CPU core。仍需評估影片資料傳輸方式、FFmpeg 是否自己已經多執行緒、 同時編碼數與記憶體/磁碟頻寬。

Coroutine 與 Task 的最小實驗

import asyncio
async def job(name: str, delay: float) -> str:
    print(f"{name}: start")
    await asyncio.sleep(delay)
    print(f"{name}: done")
    return name
async def sequential() -> None:
    # 第一個完成後才開始第二個
    await job("A", 2)
    await job("B", 1)
async def concurrent() -> None:
    # 先建立兩個獨立排程單位
    task_a = asyncio.create_task(job("A", 2))
    task_b = asyncio.create_task(job("B", 1))
    await task_a
    await task_b
asyncio.run(concurrent())

asyncio 與 blocking library 的橋接

import asyncio
import requests
def blocking_fetch(url: str) -> int:
    response = requests.get(url, timeout=10)
    return len(response.content)
async def main() -> None:
    # blocking_fetch 在 ThreadPool 的 Thread 執行,
    # event loop Thread 不會被 requests.get 卡住。
    size = await asyncio.to_thread(
        blocking_fetch,
        "https://example.com",
    )
    print(size)
asyncio.run(main())

asyncio 協調 ProcessPool

import asyncio
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(number: int) -> int:
    return sum(value * value for value in range(number))
async def main() -> None:
    loop = asyncio.get_running_loop()
    with ProcessPoolExecutor() as pool:
        results = await asyncio.gather(
            loop.run_in_executor(pool, cpu_heavy, 20_000_000),
            loop.run_in_executor(pool, cpu_heavy, 20_000_000),
        )
    print(results)
if __name__ == "__main__":
    asyncio.run(main())

13. 實際開發時怎麼選?先判斷瓶頸,再選抽象層

不要先問「哪一個最快」。 先問等待點在哪裡、誰擁有資源、是否需要共享記憶體、是否要使用多核心、失敗時需要什麼隔離。
第一步:工作主要是在「等待」還是在「計算」? 等 API、DB、WebSocket、socket → I/O-bound。影像、編碼、壓縮、巨大迴圈 → CPU-bound。
第二步:I/O library 有 async API 嗎? 有 → 使用 coroutine + Task。沒有 → 使用普通 defasyncio.to_thread() 包裝 blocking call。
第三步:CPU-heavy 工作是否要使用多核心? 是 → ProcessPool、multiprocessing、Celery worker 或其他獨立運算服務。
第四步:工作之間是否需要共享大量可變狀態? Task/Thread 共享容易但要防 race condition;Process 隔離較安全,但資料交換成本較高。
第五步:不要只看「能不能跑」,要看資源上限。 Task 要限制 concurrency;Thread 要限制 pool size;Process 通常以 CPU core、記憶體和 I/O 頻寬決定 worker 數。

快速選擇表

  • FastAPI 同時呼叫三個獨立 API:TaskGroup。
  • 處理 10,000 個 WebSocket 連線:asyncio Tasks。
  • 只有同步版本的第三方 SDK:asyncio.to_thread() 或 ThreadPool。
  • 大量圖片縮放:ProcessPool 或外部 worker。
  • 影片轉檔:通常交給 FFmpeg subprocess/worker,不在 event loop 直接算。
  • 單純先後相依的 async 操作:直接依序 await,不必 create_task。

14. 自我檢查:確認直覺已能轉成工程術語

題目 1:create_task 會建立新 Thread 嗎?

題目 2:在 async def 裡執行五秒純 Python 大迴圈,其他 Tasks 可以自動流暢執行嗎?

題目 3:純 Python CPU-heavy 工作最典型的選擇?

題目 4:Coroutine 與 Task 的關係?

題目 5:一個 Uvicorn worker 主要只有一條 event-loop thread,代表一次只能處理一個 request 嗎?

題目 6:把 asyncio loop 換成 uvloop,等於增加一條 Thread 嗎?

題目 7:asyncio 本身是否只代表 SelectorEventLoop?

題目 8:uvloop 與 asyncio 的關係?

題目 9:Selector 與 Proactor 主要差在哪裡?

題目 10:Runtime 是不是像 Thread 一樣的一個固定 OS 物件?

題目 11:CPython runtime 與 asyncio runtime 的關係?

題目 12:直接 await child() 時,child 通常會自動成為新的 Task 嗎?

題目 13:Coroutine 與 Task 分離最核心的設計理由?

題目 14:哪種情況最適合建立 Task?

延伸問題:為什麼 Task 仍需要 Lock?
因為 Task A 可以讀取共享狀態後在 await 暫停,Task B 接著修改同一狀態; Task A 恢復後可能用過期資料寫回。asyncio 是單 Thread 不代表所有多步驟操作都具有原子性。
延伸問題:Task 與 Thread 哪個比較「快」?
沒有單一答案。Task 的建立與切換成本通常更低,適合大量 async I/O; Thread 可直接執行 blocking library。真正瓶頸取決於 I/O、CPU、連線池、外部服務限制與共享狀態設計。

15. 官方來源

教材以官方文件與專案文件為主要依據;比喻與分層順序是為初學者重新組織, 正式行為仍以實際 Python/Uvicorn/uvloop 版本文件為準。