1. 全篇閱讀架構:先聽懂,再接上工程術語
只用比喻會產生一個問題:你覺得自己「好像懂了」,但看到文件裡的
Task、Future、selector、callback、event-loop implementation
時仍然接不上。反過來,只堆工程名詞又會讓初學者不知道每個元件到底在解決什麼。
1先建立四個概念所在的層級
Coroutine、Task、Thread、Process 不是同一層的四個替代選項。 它們分別回答不同問題:
2從比喻正式轉成工程模型
工程術語橋接
「可暫停的工作流程」正式叫 coroutine; 「event loop 可以管理的一份進行中工作」叫 Task; 「實際執行機器指令的 OS execution context」叫 Thread; 「具有獨立虛擬位址空間與資源的程式執行個體」叫 Process。
3先看整體資料流
2. Runtime 到底是什麼?它不是單一元件,而是「執行中的支援環境」
1生活直覺:程式碼像劇本,runtime 像整套演出系統
一份劇本只描述角色要說什麼、何時進場,但劇本自己不會演出。 真正演出時還需要舞台、演員、燈光、道具、場務、時間控制與錯誤處理。
把它換成程式:
| 劇場比喻 | 工程術語 | 作用 |
|---|---|---|
| 劇本規則 | Python language syntax/semantics | 定義 def、async def、await 等語法代表什麼 |
| 整套演出系統 | Runtime | 讓程式在執行期間真的能建立物件、呼叫函式、處理例外與管理資源 |
| 一間劇場 | Process | 提供獨立記憶體與 OS 資源 |
| 正在工作的演員/工作人員 | Thread | 實際執行機器指令與 Python code |
| 非同步場務與派場系統 | asyncio runtime/event loop machinery | 管理 Tasks、Futures、timers 與 I/O 通知 |
從比喻接到正式工程定義
Runtime(執行期環境)是個統稱,指程式「正在執行時」,
為它提供執行能力、資料狀態、資源管理與服務的一組軟體機制。
它不是像 Task、Thread 一樣必然對應到單一物件。
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。
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。
Process、Threads、virtual memory、sockets、timers
OS 提供的資源與隔離容器
執行 bytecode、管理 objects、frames、exceptions、memory 與 interpreter state
進入 interpreter context,實際執行 Python code
event loop、Tasks、Futures、callbacks、timers、executors
request Tasks、dependencies、DB clients、business logic
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 與版本。
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 上。 因此兩者不是競爭關係:
asyncio、event loop 與 runtime 的關係
run()、Task、Future、Queue、Lock、streams。epoll、kqueue、IOCP、Threads、sockets。5ASGI server runtime 又是什麼?
當你執行 Uvicorn 時,還會多一層 server runtime。它負責把網路世界與 FastAPI application 接起來:
- 建立 server Process、event loop 與 listening socket。
- 接受 TCP connections,解析 HTTP/WebSocket protocol。
- 依照 ASGI 規格建立
scope、receive、send。 - 把 FastAPI application 作為 async callable 呼叫並等待。
- 處理 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
- OS 建立四個 worker Processes。
- 每個 Process 啟動一份 CPython runtime/interpreter state。
- CPython 執行 Uvicorn server code。
- Uvicorn 在每個 worker 選擇 uvloop 作為 event-loop implementation。
- asyncio 的 Task/Future/awaitable 模型建立在該 loop 上運作。
- Uvicorn 將 HTTP/WebSocket events 轉成 ASGI messages。
- FastAPI endpoint coroutine 被 request Task 推進。
- uvloop/libuv 再透過 OS I/O 機制等待 sockets 與 timers。
6同一句 runtime,在不同上下文可能指不同東西
| 常見說法 | 通常指什麼 | 在本教材的位置 |
|---|---|---|
| Python runtime | 正在執行 Python 的 interpreter、object system、memory 與 runtime state | Process 內,支撐所有 Python 程式 |
| CPython runtime | Python runtime 的 CPython 實作 | Python Process 內的核心執行環境 |
| asyncio runtime(方便總稱:loop、Tasks、Futures 等) | event loop、Tasks、Futures、callbacks、timers、executors 的協作機制 | CPython runtime 之上、application 之下 |
| ASGI server runtime | Uvicorn 等 server 在執行期間負責 connection、protocol、ASGI calling 與 lifecycle 的系統 | 網路與 FastAPI application 之間 |
| Application runtime | 應用程式正在運作時的 dependencies、pools、caches、background Tasks 與 state | FastAPI application 自己的執行中環境 |
| Runtime error | 程式實際執行到某處才發生的錯誤 | 描述錯誤發生時機,不是一個軟體元件 |
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 |
3. Event Loop 完整拆解:它是什麼、做什麼、有哪些實作?
1生活直覺:event loop 像學校總務台
想像總務台同時處理很多事情:冷氣報修、包裹到校、教室鑰匙申請、影印完成通知。 總務人員不會在冷氣維修員抵達前一直站在門口等;他會先記下這件事,去處理其他目前能做的工作。 當電話、廣播或系統通知「維修員到了」,再接續原本的流程。
總務台反覆做的事是:
- 查看目前有哪些事情已經可以處理。
- 選一件事情,執行它的下一小段。
- 若需要等待外部結果,就登記「結果到了再通知我」。
- 繼續處理其他 ready 的事情。
- 沒有事情可做時,睡到下一個通知或計時器到期。
從比喻接到工程術語
總務台的「待處理清單」對應 ready queue; 一件進行中的工作對應 Task; 「兩秒後再處理」對應 timer/scheduled callback; 「網路資料到了再通知」對應 OS I/O event; 負責反覆接通知與派發工作的核心,就是 event loop。
2High-level 工程定義
它的主要用途可以拆成四類:
排程可執行工作
執行 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 在做什麼?
以下是適合初學者使用的簡化模型:
- 搬移到期 timers:把已到時間的 scheduled callbacks 放入 ready queue。
- 執行 ready work:逐一執行 ready callbacks,或讓 Task 推進 coroutine 一小段。
- Task 遇到等待:若 coroutine await 的 Future 尚未完成,Task 暫停,並在 Future 上登記喚醒 callback。
- 計算可睡多久:依最接近的 timer、是否已有 ready work 等條件決定 I/O poll timeout。
- 等待 OS event:讓 selector/proactor/libuv 等底層機制等待 I/O 或 completion。
- 接收通知:將完成的 I/O、timer、thread-pool 結果轉成 callbacks,放回 ready queue。
- 下一輪:只要還有活動中的工作,loop 就繼續重複。
callbacks/Tasks 可立即執行
執行一批 ready work
等待 I/O/completion
重新進入 ready queue
把事件轉成 Python callback
socket ready/operation completed
工程上不同 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() |
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 的整合。
async def、await:語言層級的 coroutine 語法run、TaskGroup、Queue、streams:application 常用介面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 是另一種嗎?
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:
async def、await、TaskGroup
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。
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 |
4. FastAPI Worker:一個 Process、主要 Thread、Event Loop 怎麼接起來?
1生活直覺與正式術語
把一個 Uvicorn worker 想成一間獨立分店。分店內有一位主要櫃台人員,使用訂單系統協調很多顧客; 某些舊設備只能讓人站在旁邊等,這時才叫支援人員處理。客流再大時,不是讓同一位櫃台人員長出四雙手, 而是多開幾間分店。
工程術語橋接
分店=worker Process;主要櫃台人員=Main Thread; 訂單系統=event loop;訂單=Task; 支援人員=thread-pool Threads;多開分店=multiple worker processes。
2High-level 架構
獨立 memory、interpreter、global state、pools
執行 event loop
按需執行 blocking work
Tasks、timers、network events
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 上。
Loop 1
Loop 2
Loop 3
Loop 4
uvloop 放在這個架構的哪裡?
uvloop 只替換每個 worker 裡的 event-loop implementation,不會改變 worker 數量、Thread 數量或 endpoint 語法。
--workers調整 Process 數量與多核心容量--loop選擇每個 worker 內 event-loop implementation5. Coroutine:可以暫停並保留現場的工作流程
1生活直覺與術語橋接
普通函式像一段必須一路跑完的關卡;coroutine 像可以存檔的關卡。 它暫停時會保留「走到哪裡」與「身上有哪些資料」,等等待條件完成後再從原處繼續。
工程術語橋接
「關卡設計圖」是 coroutine function;呼叫後產生的「這一次遊戲進度」是 coroutine object;目前執行位置、local variables 與 exception state 構成可恢復的執行狀態。
2High-level 定義
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。
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。
6. Coroutine 與 Task 的關係:為什麼有 Coroutine 之後還需要 Task?
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 表示 | 未來才會完成的低階結果 |
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 的典型狀態可以理解成:
已建立但未開始
正在執行
停在 await
被恢復
return/raise/close
這些狀態描述的是「函式執行進度」。但是它沒有完整回答: 何時開始、由誰恢復、是否獨立取消、結果由誰保存、錯誤由誰負責收走。
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 明確持有與等待。
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」,而是:
直接 await 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())
現在結構變成:
parent coroutine
child coroutine
create_task() 不是讓 coroutine「比較 async」;
它是建立新的排程與生命週期邊界,使 child 可以和 parent 交錯前進。
# 多數情況下沒有必要
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()
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
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 建立新的排程單位。
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 時:
- Parent Task 暫停。
- Child Task 繼續由 event loop 獨立排程。
- Child Task 完成後,自己的 Future-like 狀態變成 done。
- 完成 callback 讓 Parent Task 重新進入 ready queue。
- Parent Task 恢復,取得 Child Task 保存的 result 或 exception。
await child_task
獨立執行
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 沒有產生價值 |
快速決策流程
await。
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 表達可暫停的計算;Task 為某一次 coroutine 執行加入 scheduling identity、 Future-compatible completion state、cancellation、context 與 lifecycle ownership。
本章官方依據
7. Task:event loop 管理 coroutine 的工作單位
1生活直覺與工程術語
Coroutine 像一份辦案流程;Task 是把某一份流程正式立案後的案件。 立案後系統可以知道案件是否待處理、正在等待、完成、失敗或被取消。
工程術語橋接
「立案」是建立 Task;「案件流程」是 coroutine; 「案件結果」是 Task result;「辦案失敗原因」是 exception; 「撤案請求」是 cancellation。
2High-level 定義
task = asyncio.create_task(fetch_user(), name="fetch-user")
user = await task
3底層運作
- Task 被建立並排入 event loop 的 ready work。
- Event loop 執行 Task;Task 推進 coroutine。
- Coroutine await 尚未完成的 Future,Task 暫停。
- Task 在該 Future 上登記 wake-up callback。
- Future 完成後 callback 讓 Task 重新進入 ready queue。
- 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。
8. Thread:Process 內真正執行指令的一條路徑
1生活直覺與術語橋接
多條 Threads 像同一間廚房裡的多位廚師:大家能直接拿同一台冰箱裡的材料,溝通很快; 但也可能同時修改同一鍋湯,造成資料競爭。
工程術語橋接
共用廚房是 shared address space/heap;每位廚師自己的工作步驟與暫存資料是 call stack;作業系統臨時切換哪位廚師工作是 preemptive scheduling; 保護共用鍋子的規則是 Lock/synchronization。
2High-level 定義
3與 Task 的核心差異
| 面向 | asyncio Task | OS Thread |
|---|---|---|
| 排程者 | Event loop | Operating 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。
9. Process:具有獨立虛擬記憶體與執行資源的程式個體
1生活直覺與術語橋接
不同 Processes 像不同分店。每間店有自己的庫存與工作台;一間店修改庫存,不會直接改到另一間。 要交換物品必須透過物流或正式通訊管道。
工程術語橋接
分店自己的倉庫是 virtual address space;自己的 Python 執行環境是 interpreter/CPython runtime state;分店間物流是 IPC; 把 Python object 包裝成可傳輸資料是 serialization/pickling。
2High-level 定義
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,需要避免過度並行。
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。程式設計者相對容易知道可能在哪些位置切換。
Thread / Process:搶佔式切換
作業系統可以在時間片用完、Thread 阻塞或其他排程條件出現時切換。 從 application 角度看,切換點比較難預測。
12. 程式範例:從需求判斷應使用哪一層
以下每個例子都依固定順序閱讀:先判斷工作是在等待還是在計算, 再決定使用 coroutine/Task 表達流程、Thread 橋接 blocking API,或 Process 取得多核心平行。
需求:同時查詢 100 個遠端 API
Coroutine 負責什麼?
await 標出等待點。但只有 coroutine object 還不代表 100 個工作已開始 concurrent 執行。
Task 負責什麼?
Thread 能不能做?
Process 適合嗎?
需求:將四支影片做 CPU-heavy 編碼
Coroutine / Task 適合嗎?
Process 適合嗎?
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. 實際開發時怎麼選?先判斷瓶頸,再選抽象層
def 或
asyncio.to_thread() 包裝 blocking call。
快速選擇表
- 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?
await 暫停,Task B 接著修改同一狀態;
Task A 恢復後可能用過期資料寫回。asyncio 是單 Thread 不代表所有多步驟操作都具有原子性。
延伸問題:Task 與 Thread 哪個比較「快」?
15. 官方來源
教材以官方文件與專案文件為主要依據;比喻與分層順序是為初學者重新組織, 正式行為仍以實際 Python/Uvicorn/uvloop 版本文件為準。
- Python:Execution model(interpreter 與 Python runtime)
- Python:Python Runtime Services
- ASGI:Main specification(scope、receive、send)
- ASGI:Lifespan Protocol(startup/shutdown runtime context)
- Python:asyncio — Asynchronous I/O
- Python:Coroutines and Tasks
- Python:Event loop APIs 與 implementations
- Python:asyncio Platform Support(Windows/macOS)
- Python:selectors — High-level I/O multiplexing
- Python:threading — Thread-based parallelism
- Python:multiprocessing — Process-based parallelism
- Python:ThreadPoolExecutor/ProcessPoolExecutor
- FastAPI:Concurrency and async / await
- FastAPI:Server Workers
- Uvicorn:Event Loop 選擇與 custom loop
- Uvicorn:Settings(--loop、--workers)
- uvloop 官方專案:asyncio drop-in replacement、Cython、libuv
- libuv:Design overview 與 I/O loop
- libuv:Thread pool work scheduling