Python 非同步 20 年演進史
與 Go 或 Erlang 這種自誕生起便內建排程器與輕量執行緒的現代語言不同,Python 誕生於 1990 年代初。它的語言哲學、記憶體模型、全域直譯器鎖(GIL)以及廣大生態,全數根植於單執行緒、同步阻塞與 C 語言擴充套件相容性。
這項歷史前提決定了 Python 發展非同步時無法推倒重來,只能借用直譯器既有的機制(例如產生器)、疊加語法糖與標準函式庫設計,不斷尋求妥協。
這是一段長達二十餘年的「漸進式突圍」實錄:從早期以 select 為核心的 asyncore,到藉由產生器雙向通訊的「借道轉型」,再到社群框架的顯式與隱式路線分裂;最終由 Python 之父 Guido van Rossum 親自主導 Tulip 專案,確立官方 asyncio 標準。
然而,語法的標準化遠非故事終點。現代非同步架構帶來了函式著色的生態撕裂、threading.local 失效引發的跨請求狀態污染,以及缺乏約束的非結構化並行。
直到 Trio 發起結構化並行革命,Python 才在 3.11 迎來 TaskGroup。隨著 Python 3.13 推進可選的 Free-threading 建置(PEP 703 允許停用 GIL),並於 3.14 依 PEP 779 轉為正式支援,非同步協程與多執行緒的定位更面臨全新的重估。
一、史前時代與協程演進:從 asyncore 到產生器借道轉型
1. asyncore 的極限與狀態機泥淖
在 Python 2.x 早期,標準函式庫中處理非同步網路通訊的模組,是由 Sam Rushing 在 1990 年代編寫的 asyncore 與 asynchat(已於 Python 3.10 標記棄用,3.12 正式移除)。
asyncore 的核心邏輯非常原始:它將 Socket 物件封裝成 dispatcher 類別,底層維護全域 Socket 字典,並在主迴圈中呼叫作業系統的 select() 或 poll()。
在沒有協程概念的年代,這種設計帶來了沉重的工程包袱:開發者必須在類別實例上手動維護狀態機,通訊協定的交握、資料交換與逾時重試被硬生生割裂成離散的回呼片段;加上底層深度綁定傳統的 select(),受限於 FD_SETSIZE 的 1,024 檔案描述符硬上限與 O(N) 輪詢開銷,完全無法應對現代高並行需求。
2. 產生器的三次質變:協程的借道突圍
在不破壞直譯器既有排程架構的前提下,Python 核心團隊找到了一條巧妙的借道方案:將原本用於惰性計算的產生器(Generator),逐步擴充為具備上下文暫停與恢復能力的協程。這場變革經歷了三個關鍵 PEP:
- PEP 255(Python 2.2,2001 年):引入
yield關鍵字,允許函式在執行中途產出值並保存當前的 Execution Frame(包含區域變數與指令位置)。但此時的產生器是單向管道,外部呼叫者只能透過next()取值,無法反向傳入資料。 - PEP 342(Python 2.5,2006 年):將
yield從陳述式升級為運算式,並新增.send(value)、.throw()與.close()三個方法。外部事件迴圈可在 I/O 阻塞時讓產生器yield讓出 CPU,待資料就緒後透過.send()注入資料並恢復執行。這正式確立了 Generator-based Coroutines。 - PEP 380(Python 3.3,2012 年):引入
yield from <expr>語法糖。它在呼叫者與子產生器之間建立了全自動的雙向透明穿透管道,呼叫者的send()與throw()會直接穿透傳遞給子產生器,子產生器的回傳值則透過StopIteration(expr)指派給運算式,徹底解決了巢狀委派難題。
3. 從產生器模擬到原生語法對照
PEP 380 讓產生器模擬非同步呼叫鏈的架構完全成熟,為 asyncio 的誕生鋪平了最後一哩路。但基於產生器的協程本質上仍是產生器,直譯器無法在型別層面防範誤用。
以下對照 Python 3.4 基於裝飾器與 yield from 的舊式非同步程式碼,與 Python 3.5+ PEP 492 原生語法的差異:
# 方案 A:Python 3.4 時代(PEP 380 + PEP 3156,基於產生器)
import asyncio
@asyncio.coroutine
def fetch_data_legacy(url: str):
print(f"[Legacy] 開始擷取: {url}")
yield from asyncio.sleep(1)
return f"Data from {url}"
@asyncio.coroutine
def main_legacy():
data = yield from fetch_data_legacy("https://example.com")
print(f"[Legacy] 取得結果: {data}")
# 方案 B:Python 3.5+ 時代(PEP 492,原生協程)
async def fetch_data_native(url: str) -> str:
print(f"[Native] 開始擷取: {url}")
await asyncio.sleep(1)
return f"Data from {url}"
async def main_native():
data = await fetch_data_native("https://example.com")
print(f"[Native] 取得結果: {data}")
在方案 A 中,fetch_data_legacy 回傳的是普通的 generator 物件。若開發者誤將其寫入 for item in fetch_data_legacy(),直譯器無法發出警告,導致邏輯無聲崩潰。
方案 B 引進的 async def 則宣告了專屬的 coroutine 型別。直譯器嚴格禁止將其當作迭代器(Iterator),且若未加上 await 便會觸發 RuntimeWarning: coroutine was never awaited,在型別與語意上,協程從此與產生器徹底分家。
二、早期路線大分裂:Twisted、Tornado 與 Gevent 的顯隱之爭
在官方 asyncio 標準確立之前,Python 社群在 2000 至 2012 年間展開了激烈的路線探索。這場爭論圍繞著「顯式(Explicit)還是隱式(Implicit)」以及「回呼(Callback)還是協程(Coroutine)」展開,形成了三個代表性框架。
1. Twisted 與回呼地獄:Reactor 模式的先驅
創始人 Glyph Lefkowitz 於 2001 年發起 Twisted,比 Node.js 早了約八年。Twisted 奠定了事件驅動架構的經典設計:以單一全域事件迴圈(Reactor 模式)監聽 Socket 事件,並提出 Deferred 物件處理非同步完成通知。
然而,當業務邏輯包含條件分支、重試或巢狀請求時,Deferred 鏈會演變成嚴重的回呼地獄(Callback Hell)。
更致命的是堆疊遺失。所有 Callback 都在事件迴圈頂層被呼叫,一旦拋出例外,原始呼叫點的呼叫堆疊早已消失,除錯日誌只剩下排程器的雜亂回溯。
2. Tornado 的折衷:IOLoop 與早期產生器協程
2009 年 FriendFeed 開源的 Tornado 帶來了重大衝擊。Tornado 原生具備非同步 HTTP 伺服器與簡潔的 IOLoop,並在 PEP 342 增強產生器就位後,於 2.1 版(2011 年)推出產生器式協程模組 tornado.gen,隨後在 3.0 版(2013 年)加入 @gen.coroutine 裝飾器。
開發者得以用 yield 替代回呼,在單一函式內享受線性程式設計的流暢感。
但在 PEP 380 誕生之前,Tornado 的產生器無法跨層穿透,任何跨函式呼叫都必須層層包裹裝飾器,限制了大型架構的模組化發展。
3. Gevent 的黑魔法:Greenlet 與猴子補丁
與 Twisted、Tornado 堅持顯式控制不同,Gevent 選擇了誘人但爭議巨大的路線——隱式並行(Implicit Concurrency)。
Gevent 基於 C 實作的微執行緒 greenlet 與 libev/libuv,核心武器是著名的猴子補丁(Monkey Patching)。只需一行 monkey.patch_all(),就能在執行期將標準函式庫的 socket、ssl 與 time 全數替換為協程切換實作。
看似優雅的黑魔法,卻在實際專案中付出了沉重代價:
- 違背 Python 之禪:程式碼表面看似標準同步呼叫,實際上卻在任何網路 I/O 處悄悄切換上下文。開發者無法從程式碼辨識切換點,極易引發難以重現的競態條件(Race Condition)。
- C 擴充生態相容性破裂:Monkey Patching 只能攔截純 Python 模組。面對 C 語言撰寫的高效能擴充(如早期的 MySQL C-driver),底層發起阻塞呼叫時會直接卡死作業系統執行緒,連帶凍結行程內成千上萬的 Greenlet。
- 隱性依賴損壞:若第三方函式庫內部維護了特定的執行緒狀態,全域 Socket 被替換後極易引發神秘的記憶體崩潰或死結。
4. 顯式與隱式的哲學十字路口
這場對決塑造了 Python 非同步的根本走向:
| 評估面向 | 顯式陣營(Twisted / Tornado / 官方 asyncio) | 隱式陣營(Gevent) |
|---|---|---|
| 程式碼可讀性 | 透過 yield 或 await 明確標記 I/O 切換點 | 表面如同步程式碼,切換點不可見 |
| 排程心智模型 | 合作式排程(Cooperative),切換點可審查 | 偽搶佔式體驗,切換點隱藏在底層系統呼叫中 |
| 舊有程式碼改造 | 成本高:必須全鏈條改寫呼叫介面 | 成本低:通常只需在入口處加上 patch_all() |
| C 擴充相容性 | 優秀:可由開發者在專屬執行緒池明確隔離 C 呼叫 | 極差:無法攔截底層 C 語言發起的阻塞呼叫 |
| Python 之禪相容 | 完全符合:顯式優於隱式、面對歧義拒絕猜測 | 違背:依賴黑魔法偷換直譯器狀態 |
儘管 Gevent 在早期讓許多舊系統迅速享受到並行紅利,但 Guido van Rossum 與核心團隊最終斷然拒絕了隱式排程。Python 選擇堅持「顯式(Explicit)」原則,由語言與生態共同承擔語法重構的代價。
三、官方標準化之路:Tulip 啟航、Web 轉型與生態中空期
1. Guido 親征:Tulip 專案與 PEP 3156
2012 年前後,Python 3 的推廣遭遇陣痛。為了替 Python 3 打造無可替代的關鍵功能,Python 之父 Guido van Rossum 親自主導了代號為 Tulip(鬱金香) 的專案。
該專案最終收斂為 PEP 3156,並在 Python 3.4 以 asyncio 模組(初期為暫定 API)正式併入標準函式庫。
PEP 3156 確立了三大核心抽象:
- 事件迴圈介面(
AbstractEventLoop):將事件迴圈介面標準化,為後續第三方高效能迴圈(如uvloop)預留了抽換空間。 - 傳輸與協定分離(Transports and Protocols):借鑑 Twisted 的優秀設計,將底層 Socket 傳輸與上層協定解析解耦。
- Future 與 Task 模型:統一了非同步操作的狀態封裝(
asyncio.Future)與協程執行單元(asyncio.Task)。
2. PEP 492 原生語法:進入現代非同步時代
Python 3.4 雖然確立了標準函式庫,但 @asyncio.coroutine 與 yield from 依然充斥著妥協感。
2015 年,由 Yury Selivanov 主導的 PEP 492 在 Python 3.5 正式引進原生語法:
async def:宣告原生協程函式。await:專屬運算子,僅接受實作了__await__()的 awaitable 物件。async for與async with:將非同步迭代器(__anext__)與非同步上下文管理器(__aenter__/__aexit__)提升為語言一等公民。
PEP 492 徹底確立了現代 Python 非同步的心智模型。隨後 Python 3.6 的 PEP 525(非同步產生器)與 PEP 530(非同步推導式),更進一步完備了語法拼圖。
3. Web 典範轉移:從 WSGI 到 ASGI
Web 是 Python 最廣泛的戰場。自 2003 年確立的 WSGI 規範(PEP 333,2010 年為 Python 3 更新為 PEP 3333)本質上是同步阻塞的:每個請求必須佔用一個實體行程或執行緒。當 WebSocket 與長連線串流爆發時,WSGI 顯得捉襟見肘。
2016 年,Django 核心開發者 Andrew Godwin 為了解決 Channels 的即時通訊需求,主導設計了 ASGI(Asynchronous Server Gateway Interface)。
ASGI 3.0 確立以單一非同步 Callable 為核心:接收 scope、receive 與 send 三個參數。這個簡潔的非同步介面,為 Uvicorn、Starlette 與 FastAPI 等現代框架奠定了標準基礎。
NOTE
4. uvloop 的突破:Cython 與 libuv 逼近原生 I/O 極限
長期以來,社群對 Python 非同步的質疑在於:「直譯器效能有限又受制於 GIL,換成非同步真能變快嗎?」
2016 年,MagicStack 團隊的 Yury Selivanov 開源了 uvloop。它使用 Cython 將 Node.js 底層歷經考驗的 libuv C 函式庫,封裝為 asyncio.AbstractEventLoop 的直接實作。
uvloop 完全繞過了 Python 官方用純 Python 與部分 C 撰寫的預設迴圈。基準測試顯示,在更換為 uvloop 後,HTTP 與 TCP 處理效能普遍提升了 2 至 4 倍,在簡單 I/O 壓測中逼近 Node.js 與 Go,為 Python 高效能服務注入了強大信心。
5. 滯後的生態鏈:ORM 與資料庫驅動的雙軌陣痛
然而,當 Web 框架在高並行跑分榜上名列前茅時,一線開發者卻面臨一個殘酷的現實:Web 伺服器是非同步的,但後端資料庫全都是同步的。
在主流業務中,絕大部分時間都在與關聯式資料庫通訊。舊有的驅動(如 psycopg2、MySQLdb)底層全為阻塞式 C 呼叫;同時,ORM 的核心機制深植於同步語意:
- 延遲載入(Lazy Loading):依賴
__getattr__魔術方法,但在 Python 規範中,屬性存取無法插入await。 - 連線池與交易:過去完全綁定在
threading.local上。
在 2018 至 2021 年間,開發者被迫在兩種痛苦間二選一:要麼放棄 SQLAlchemy 成熟的模型關聯與遷移生態,改用輕量非同步查詢庫;要麼在非同步 View 內透過 run_in_executor 將同步 ORM 丟進執行緒池,承受高昂的切換成本。
雖然 MagicStack 於 2016 年開源的 asyncpg 帶來了數倍於傳統驅動的吞吐量,但高階 ORM 的翻新難度遠超預期。直到 SQLAlchemy 1.4 / 2.0(2021–2023 年),Mike Bayer 帶領團隊歷經數年重構底層,透過 AsyncSession 與 greenlet 技術完成內部轉化,官方生態才補齊這塊關鍵缺口。
這段五年的生態空窗期表明:語言層面的語法標準化往往只是第一步,周邊驅動與 ORM 生態的重塑才是工程落實最耗時的環節。
四、深水區的語言困境:函式著色、狀態污染與取消陷阱
當非同步進入生產環境深水區後,Python 開發者遇到的是語言層與執行期的結構性挑戰。
1. 函式著色問題(What Color is Your Function?)
2015 年,Bob Nystrom 在經典文章《What Color is Your Function?》中指出:顯式協程語言必然面臨生態被切分為「紅色函式(非同步)」與「藍色函式(同步)」的宿命。
在 Python 中,這項矛盾尤為尖銳:
- 藍色不能呼叫紅色:同步函式無法直接呼叫
async def並取得回傳值;若在既有事件迴圈內呼叫asyncio.run(),會立刻引發RuntimeError: asyncio.run() cannot be called from a running event loop。 - 染色向上傳染:底層 I/O 一旦改為
async def,整個呼叫鏈的上游所有函式全部被迫加上async def與await。 - 生態函式庫分裂:社群被迫將所有網路函式庫重寫兩份:
requests(藍色)與httpx(紅色);redis-py(同步)與redis.asyncio(非同步)。
2. 跨請求狀態污染:threading.local 的瓦解與 PEP 567
在同步多執行緒時代,全域上下文傳遞的唯一標準是 threading.local()。開發者將 Request ID、登入使用者或租戶資訊存入其中,即可在呼叫鏈深處隨時讀取。
但在單執行緒協程環境下,threading.local() 會引發災難性的跨請求資料污染:
- 單一作業系統執行緒內交替執行成百上千個協程。
- 協程 A 將使用者 ID 寫入
local.user_id = 101,隨後因await讓出 CPU。 - 協程 B 取得控制權,覆寫了
local.user_id = 202。 - 當協程 A 恢復執行時,從
local讀出的竟是協程 B 的使用者身分!在權限驗證與金流情境下,這是致命的資安事故。
CAUTION
協程環境不可用 threading.local() 存放請求級狀態:登入身分、租戶或 Request ID 可能被後續請求覆寫,在權限驗證與金流場景構成跨請求資料外洩。請改用 contextvars.ContextVar(PEP 567)隔離。
為了解決這個危機,Yury Selivanov 於 Python 3.7 提出了 PEP 567,引入 contextvars.ContextVar。它讓直譯器支援「隨協程樹複製與繼承」的上下文快照機制。
以下程式碼對照了兩者的核心行為差異:
import asyncio
import threading
import contextvars
# 錯誤示範:協程環境下使用 threading.local (引發跨協程狀態篡改)
thread_local = threading.local()
async def handler_with_thread_local(req_id: str):
thread_local.req_id = req_id
await asyncio.sleep(0.1) # 讓出 CPU,其他協程極可能覆寫此值
print(f"預期: {req_id}, 實際取到: {thread_local.req_id}")
# 正確做法:使用 PEP 567 contextvars.ContextVar (協程樹安全隔離)
request_context: contextvars.ContextVar[str] = contextvars.ContextVar("request_context")
async def handler_with_context_var(req_id: str):
token = request_context.set(req_id) # 當前協程 Task 獨立 Context 快照
try:
await asyncio.sleep(0.1)
print(f"預期: {req_id}, 實際取到: {request_context.get()}")
finally:
request_context.reset(token)
在 threading.local 模型下,後抵達的請求會在協程讓出 CPU 時覆寫全域狀態,導致先前請求甦醒時讀取到錯誤資料。改用 contextvars 後,直譯器在衍生任務時會對上下文進行獨立快照,各協程修改互不干擾,徹底化解了微服務分散式追蹤與請求上下文傳遞的難題。
contextvars 的隔離邊界本質是 Task。事件迴圈在透過 create_task() 生成新任務時會自動對當前 Context 進行快照複製;因此,只要 Web 框架為每個 HTTP 請求指派獨立 Task,就能自然達成請求間的狀態隔離。
3. 例外與取消(Cancellation)的語意陷阱
非同步程式設計中最棘手的部分,往往落在連線中斷或逾時觸發時的取消(Cancellation)語意。在 asyncio 中,取消一個 Task 的底層機制是在協程暫停點注入 asyncio.CancelledError。
這個機制伴隨著兩個惡名昭彰的陷阱:
- 常規捕捉意外吞噬取消訊號:在 Python 3.7 之前,
CancelledError繼承自Exception。習慣撰寫except Exception: pass的工程師會無意間吞掉取消例外,導致任務拒絕終止,成為背景常駐的幽靈任務。(Python 3.8 將其改為繼承自BaseException,才避開誤捕)。 - 清理階段中斷與保護陷阱:在
finally區塊中進行非同步清理(例如await db.rollback())時,若清理期間外層再度觸發取消或逾時,會中斷清理邏輯導致資源洩漏。過去常嘗試以asyncio.shield()保護清理協程,但shield()僅保護被包裝的協程在背景執行,當前任務依然會立刻捕獲CancelledError,若未妥善處理例外傳遞,極易造成執行期混亂;在現代架構中,更推薦透過專屬的逾時保護或獨立作用域(Scope)收尾關鍵流程。
WARNING
asyncio.shield() 不會保護當前任務:它只讓被包裹的清理協程轉入背景續跑,當前任務仍會立刻捕獲 CancelledError;關鍵收尾應改用專屬逾時保護或獨立作用域,否則清理仍可能被中斷而遺留資源洩漏。
五、結構化並行革命:告別並行世界的 goto
1. 傳統並行的「Goto 危機」:Nathaniel J. Smith 與 Trio 的反思
2018 年,Trio 框架作者 Nathaniel J. Smith(njs)發表了開源社群名篇《Notes on structured concurrency, or: Go statement considered harmful》。
文章回顧了 1968 年 Edsger Dijkstra 的名篇《Go To Statement Considered Harmful》。在結構化程式設計誕生前,程式碼充斥著 goto 跳轉,導致變數生命週期混亂、呼叫堆疊支離破碎。現代語言透過迴圈、條件分支與函式呼叫徹底取代了 goto,確立了「控制流程具備明確入口與單一出口」的鐵律。
Nathaniel J. Smith 尖銳地指出:現代非同步語言中的 asyncio.create_task() 或 Go 語言的 go func(),本質上就是並行世界裡的 goto!
非結構化並行帶來了三大宿疾:
- 生命週期失控(Lifetime Smuggling):呼叫
create_task()將協程丟入全域事件迴圈後,父函式即使提早回傳退出,子任務依然在背景遊蕩,外部無法預期其結束時機。 - 例外無人承接(Silent Failure):背景任務一旦崩潰,堆疊只能印在 stderr 或被完全忽略,無法自然回饋給呼叫端重試。
- 取消機制無法自動串聯:父任務逾時或取消時,開發者必須手動巡遍並取消所有背景子任務,極度容易疏漏。
2. 結構化並行理念與 Nursery
結構化並行(Structured Concurrency)的核心原則是:並行任務必須擁有清晰的巢狀作用域邊界。父任務衍生出的所有子任務,必須在離開該作用域前全數終結(正常結束或被取消);父作用域對子任務的成敗與生命週期負有絕對責任。
這催生了「育兒室(Nursery)」模型:任何並行任務只能在明確的作用域內生成。一旦其中任一子任務引發未捕獲例外,作用域會自動向其餘所有同級任務傳送取消訊號,在收集所有例外後,將錯誤統一向外層拋出。
3. 從 asyncio.gather 到 Python 3.11 asyncio.TaskGroup
Python 社群深刻吸納了 Trio 的哲學。在引入 PEP 654(Exception Groups and except)* 後,Python 3.11 正式在標準函式庫推出了 asyncio.TaskGroup。
以下對照傳統 asyncio.gather 的失控風險,與現代 TaskGroup 的嚴格結構化約束:
import asyncio
async def worker_ok(name: str):
await asyncio.sleep(2)
print(f"[{name}] 順利完成!")
async def worker_fail(name: str):
await asyncio.sleep(0.5)
print(f"[{name}] 發生致命崩潰!")
raise ValueError(f"{name} 失敗")
# 傳統作法:asyncio.gather (非結構化,存在背景任務逃逸)
async def test_gather():
print("--- 測試 legacy gather ---")
try:
await asyncio.gather(worker_ok("Task-1"), worker_fail("Task-2"))
except Exception as e:
print(f"[gather 捕捉到例外]: {e}")
# 注意:此時 Task-1 依然在背景悄悄運行,生命週期已脫離控制!
await asyncio.sleep(2.0)
# 現代標準:Python 3.11+ asyncio.TaskGroup (結構化並行)
async def test_taskgroup():
print("\n--- 測試現代 TaskGroup ---")
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(worker_ok("TG-1"))
tg.create_task(worker_fail("TG-2"))
except* ValueError as eg:
print(f"[TaskGroup 捕捉到例外群組]: {eg.exceptions}")
print("核心保證:任一任務崩潰,同 Scope 內其餘任務立即連鎖取消,避免殘留未受控的孤立任務。")
async def main():
await test_gather()
await test_taskgroup()
if __name__ == "__main__":
asyncio.run(main())
在 test_gather 中,worker_fail 崩潰後,worker_ok 依然在背景持續執行直到第 2 秒。而在 TaskGroup 中,TG-2 失敗的瞬間,作用域自動向 TG-1 發出取消訊號,並將錯誤透過 PEP 654 ExceptionGroup 完整向上聚合,杜絕了一切幽靈任務。
此外,Alex Grönholm 開發的 AnyIO 相容層,讓開發者只需撰寫一套符合結構化並行規範的程式碼,即可直接在 asyncio 或 trio 上運作。目前 Starlette 與 HTTPX 均直接採用 AnyIO 作為非同步相容層。
六、終局思辨:No-GIL 時代的非同步定位與分層架構
隨著 Python 3.13 引入實驗性的 Free-threaded CPython(PEP 703,允許停用 GIL 的建置選項),並在 3.14 依 PEP 779 轉為正式支援,社群開始重新審視多執行緒的競爭力:當執行緒能實現真正的多核平行運算時,開發者是否還需要承擔 async/await 的心智負擔?
事實上,非同步協程與多執行緒所處理的效能瓶頸截然不同,兩者在架構上並非取代,而是分工互補。
1. 原生執行緒與非同步協程的本質差異
從系統資源與排程模型檢視,兩者存在根本分歧:
| 架構指標 | 作業系統原生執行緒(OS Threads / No-GIL) | 非同步協程(Async Coroutines / asyncio) |
|---|---|---|
| 排程方式 | 搶佔式(Preemptive):由作業系統核心時鐘中斷強制切換 | 合作式(Cooperative):由程式碼在 await 點主動讓出 |
| 記憶體佔用 | 每個執行緒預設佔用 1MB 至 8MB 堆疊空間 | 每個協程本質只是 Python Frame 物件,僅佔 數百 Bytes 至數 KB |
| 並行規模極限 | 單機建立 2,000 至 5,000 執行緒即達極限(記憶體吃緊、核心排程崩潰) | 單機可輕鬆維持 50,000 至 200,000 個活躍連線狀態 |
| 切換開銷 | 核心態上下文切換(暫存器保存、快取失效、TLB 更新),成本高昂 | 使用者態函式指標跳轉,速度接近一般函式呼叫 |
| 狀態同步安全 | 風險極高:任何指令間隙均可能被中斷,需大量 Mutex/Lock 防範競爭 | 低底層競爭、仍具邏輯競態:兩個 await 間具備單執行緒原子性,無記憶體 Data Race;但跨 await 的複合操作仍需以 asyncio.Lock 防範業務狀態衝突 |
| 核心解決痛點 | 突破 GIL,利用多核 CPU 平行處理計算密集任務 | 解決 C10K/C100K 高連線、高延遲的 I/O 多工等待問題 |
2. 未來架構終局:Async 外層多工+No-GIL 內層平行池
在 No-GIL 時代成熟後,Python 的最佳架構實踐將走向分層協同(Hierarchical Hybrid):
- 外層網路通訊(Gateway / Web Layer):堅持採用 ASGI +
asyncio(uvloop)。面對大量長連線 WebSocket 與外部微服務 I/O,協程具備極低的記憶體佔用與出色的多工吞吐,這是多執行緒永遠無法比擬的。 - 內層運算密集(Compute Layer):徹底拋棄過去為了避開 GIL 而採用的笨重
multiprocessing(跨行程序列化與 IPC 開銷巨大)。非同步服務可直接透過ThreadPoolExecutor將高負載計算(如影像處理、大封包加密、複雜資料序列化)丟給原生多執行緒,在共享記憶體空間中實現真正的平行運算。
Free-threading 並非否定非同步的價值,反而補齊了非同步長久以來「一旦遇到 CPU 密集運算就卡死事件迴圈」的短板。
NOTE
延伸閱讀:Python GIL 30 年演進史
3. 架構思辨:回歸 Boring Technology
回顧這段長達二十年的演進,它給現代開發者留下了清晰的指引:
- 拒絕非同步狂熱(Avoid Async Hype):在「樸實技術(Boring Technology)」視角下,若系統核心業務是典型的 CRUD 內部管理系統、QPS 僅在數百且重度依賴複雜同步 ORM,堅持傳統的 WSGI + Gunicorn 多行程模型依然是心智負擔最低、除錯最容易的最佳選擇。切勿盲目追求潮流而承擔函式著色與除錯黑洞的額外成本。
- 擁抱結構化並行(Adopt Structured Concurrency):若確實存在高連線長 I/O 需求,應全面淘汰非受控的
asyncio.create_task()。善用 Python 3.11 的TaskGroup與 AnyIO,讓每個非同步任務都有清晰的作用域歸宿與集中的例外承接點。 - 理解語言設計的權衡代價:從產生器的借道妥協,到 PEP 492 的原生語法,再到 ContextVars 對 Thread-local 的重塑,每一個看似精妙的 API 背後,都是 Python 為了兼顧「顯式優於隱式」與向下相容性所付出的工程代價。
NOTE