一趟從 `threading` 到 `anyio` 的深度思考與解析
我們的旅程始於兩個處理併發的經典模型。它們的目標相似,但運作方式卻截然不同。
由作業系統 (OS) 決定何時切換任務,就像一位專案經理,隨時可能中斷一個員工(執行緒),讓另一個員工接手。程式本身無法控制切換時機。
由程式自己透過 await 關鍵字主動放棄控制權。就像一位自律的員工,在等待時會主動去做別的事,效率極高。
我們很快就遇到了一堵名為 GIL (全域直譯器鎖) 的高牆。它規定了在任一時刻,只有一個執行緒能執行 Python 位元組碼,這也引出了兩個關鍵概念的區別:
當我們想發起大量網路請求時,直接全部啟動會造成問題。我們發現 `threading` 的介面非常簡潔,而 `asyncio` 則需要一個關鍵工具:Semaphore。
# ThreadPoolExecutor 的簡潔寫法
with ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(task, arg) for arg in args]
# Asyncio 的標準做法,使用 Semaphore
async def worker(arg, semaphore):
async with semaphore:
# ... 執行請求 ...
await asyncio.sleep(1)
semaphore = asyncio.Semaphore(4)
tasks = [worker(arg, semaphore) for arg in args]
await asyncio.gather(*tasks)
雖然 `asyncio` 的寫法看起來較為複雜,但我們發現可以透過**自己封裝輔助函式**或使用**第三方函式庫**(如 `aiometer`)來達到同樣的簡潔效果,同時保有其高效能與高擴展性的優勢。
在深入研究 `aiometer` 的原始碼時,我們發現它並未使用 `asyncio`,而是用了 `anyio`。這引導我們了解了 Python 非同步生態的一個重要趨勢。
`anyio` 是一個非同步轉接層,目的是解決 `asyncio` 和另一個流行框架 `Trio` 之間的 API 不相容問題。它就像一個萬用轉接頭,讓函式庫作者可以撰寫一套程式碼,同時在兩種後端上運行。
`anyio` 會在**執行時期**動態偵測當前的事件迴圈是 `asyncio` 還是 `Trio`,然後在內部呼叫對應的原生函式。這是一個非常聰明的函式庫設計決策,旨在提升程式碼的相容性與可攜性。
最後,我們探討了一個終極問題:既然有 `asyncio` 和 `Trio`,我們能否在一個程式中同時使用它們?
答案是:不行,至少不應該在同一個執行緒中這樣做。
事件迴圈是其所在執行緒的唯一「主宰者」。在同一個執行緒中運行兩個事件迴圈會導致混亂。正確的做法是: