Python asyncio 核心原理
理解「單執行緒如何達成高併發」。
Python GIL (全域解釋器鎖):規定在同一個進程 (Process) 內,即使有16核心 CPU,同一時間也只能有一個執行緒在執行 python 檔案。
影響:對於 IO 密集型任務,執行緒在等待時會釋放 GIL,所以不受影響。但對於計算密集型任務,GIL 會讓多執行緒變得像單執行緒一樣慢。
解法:多開進程,每個進程都有自己獨立的 Python 解釋器以及獨立 GIL,實現並行 (Parallelism)
關鍵字: Event Loop (事件迴圈)、Coroutine (協程)、Cooperative Multitasking (協作式多工)。
核心機制:
==Event Loop==: 系統的心臟,像個不斷旋轉的圓圈,監控所有 I/O 任務。 (所有用 async 定義的函式,都被串在這個迴圈內)
==Non-blocking I/O==: 當遇到 await 時,協程會主動交出控制權,讓 Loop 去跑別的任務,而不是在那裡發呆等待。(任何非計算密集型任務 (也就是IO等待型任務) 都需要加上await)
==併發 (Concurrency) vs 並行 (Parallelism)==: asyncio 是「一人同時接五支電話」(併發),而不是「五個人同時接五支電話」。
注意:
不要在 async 函式裡用 time.sleep(),這會阻塞整個圓圈。
解決方案: I/O 阻塞用 asyncio.to_thread()將會阻塞的任務一致其餘執行緒執行,任務完成後會返回原 Event loop 執行後續任務;CPU 密集任務用 ProcessPoolExecutor 繞過 GIL。
(沒選擇用 threading (多執行緒) 的原因是:開一個執行緒會需要8MB的記憶體空間,若需要多個執行緒任務,記憶體空間容易不足,並且多執行緒是由作業系統控制執行緒切換,如果兩個執行緒同時改同一個變數,容易發生衝突。) (同時須注意的是,多執行緒還是無法解決在以IO密集型任務為主的進程內有計算密集型任務的問題,因為使用 asyncio.to_thread 切換到其餘執行緒跑計算密集任務時,此時 GIL 會在這個執行緒上,所以就算原執行緒的任務結束了,還是會因為 GIL 在計算任務執行緒上而卡住。此時可以用的是使用多進程 (multi-processing) 的方式,讓多核心去跑,實現並行 “Parallelism” )
進階語法與資源管理
逾時保護 (asyncio.wait_for): * 給任務設定「保險絲」。超時後會拋出 TimeoutError 並自動 Cancel 內部任務,防止資源洩漏。
非同步上下文管理器 (@asynccontextmanager):
- 何謂上下文管理器:上下文管理器是一種自動化的"資源生命週期管理"工具。
核心價值在於實現 三明治結構
- Setup:把環境建置好 (例如:打開檔案、獲得Redis鎖、連上資料庫等事前準備)。
- Body:主體,執行真正的業務邏輯。
- Teardown:無論過程是否出錯,保證把環境復原 (例:關閉檔案、釋放Redis鎖、斷開連線)。
有了上下文管理器,可以專注在主要的程式邏輯,不用去處理前置作業以及後續收拾。 語法:常見的
with,就是上下文管理器的標準用法。例如:
1 2 3with open("test.txt", "w") as f: # 在第一行就幫你處理好檔案的開啟 (Setup) f.write("Hello World") # 主要執行邏輯 (Body) # 離開縮排的一瞬間,檔案會自動關閉 (Teardown)如果不使用上下文管理器,必須寫成:
1 2 3 4 5f = open("test.txt", "w") # 打開檔案 try: f.write("Hello World") # 主要執行邏輯 finally: f.close() # 萬一漏寫這行,檔案就會一直被占用,造成資源洩漏 # 關閉檔案註:aenter 與 aexit:只要一個物件具備這兩個方法,他就具有上下文管理器的資格,==async with== 其實是一個自動化的包裝,若要手動進入以及退出,就必須用這兩個方法。 自動 VS 手動:- 自動:自動隱含
try ...finally,且程式碼易讀,較常用。- 手動:彈性,可以自行決定何時進入與退出,但較少用到。NSH專案中選擇手動操作: 在「動態批次鎖」場景下,可以手動呼叫 aenter 獲取一堆鎖,並在 finally 區塊手動 aexit 釋放。這解決了 async with 無法處理動態數量資源的問題。
- 何謂上下文管理器:上下文管理器是一種自動化的"資源生命週期管理"工具。
核心價值在於實現 三明治結構
非同步程式設計 單機、單進程 全部都在同一個伺服器運作。本質是讓伺服器不要空閒。
非同步架構 (Message Queue)
從「單機」升級到「分散式系統」的思維,伺服器只負責接收請求,接收到請求後放入任務隊列,依照隊伍順序去不同worker處理。
角色拆解:
- Producer (生產者): FastAPI (負責接單)。
- Broker (仲介): Redis/RabbitMQ (任務隊列)。
- Consumer/Worker (消費者): 背景程式 (負責幹活)。
優點:
解耦 (Decoupling): API 不再需要等 AI 生成完才回傳。
削峰填谷 (Traffic Shaving): 流量暴增時,請求先在 MQ 排隊,Worker 按自己的節奏消化。
水平擴展 (Scaling Out): 只要多開幾個 Worker 視窗,就能提升系統總處理能力。
若是採用非同步架構設計,當系統瞬間湧入大量請求時,請求會先由FastAPI接收,並進入任務對列內排隊,而不會直接進入系統內,造成系統崩潰。常見的工具有:Redis、RabbitMQ、Kafka(大數據)。
以NSH專案來說,當新聞量增加時,可能一次會有1000則新聞被抓進來要執行摘要,但 gemini api 沒辦法一次處理這麼大量的請求,記憶體也無法一次載入這麼多新聞,所以就需要這個中間層來介入,排隊批次上傳生成。通常還會搭配水平擴展來分流到不同的伺服器來提供服務,讓使用者能盡快獲得回應。
系統設計理論 (CAP 定理)
系統架構的權衡取捨。
C (Consistency) 一致性: 所有人看到的資料都一樣。
A (Availability) 可用性: 系統永遠有回應。
P (Partition Tolerance) 分區容錯性: 斷網時系統不崩潰。
我的取捨: Redis 鎖是偏向 CP(保證摘要不重複,即使有時要等)。
非同步架構追求的是 最終一致性 (Eventual Consistency):使用者點下去沒看到摘要沒關係(暫時不一致),5 秒後 Worker 跑完就一致了。
工具選擇 (Redis vs. RabbitMQ) 為什麼選這個?優缺點是什麼?
Redis: 簡單、快、已經在用了。適合中小規模或對可靠性要求不是「絕對不能丟」的場景。
RabbitMQ: 專業、有確認機制 (ACK)、支援複雜路由。適合大型系統、任務絕對不能丟的場景。