asyncio 是 Python 中強大的非同步程式設計工具,其核心概念是事件迴圈。我將透過由淺入深的範例,帶您理解 asyncio 的運作方式,並解析其在處理阻塞與非阻塞操作上的精妙之處。
asyncio 的 Hello World:初探事件迴圈
讓我們從一個簡單的 “Hello World” 範例開始,感受 asyncio 的魅力:
import asyncio
import time
async def main():
print(f'{time.ctime()} 您好!')
await asyncio.sleep(1.0)
print(f'{time.ctime()} 再見!')
asyncio.run(main())
asyncio.run(main())
啟動事件迴圈並執行 main()
協程。await asyncio.sleep(1.0)
模擬一個耗時一秒的操作,期間控制權交還給事件迴圈,讓其他任務得以執行。這展現了 asyncio 非阻塞的特性。
為了更深入理解 asyncio.run()
背後的機制,我將展示一個更接近底層運作的範例:
import asyncio
import time
async def main():
print(f"{time.ctime()} 您好!")
await asyncio.sleep(1.0)
print(f"{time.ctime()} 再見!")
loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_until_complete(task)
pending = asyncio.all_tasks(loop=loop)
for task in pending:
task.cancel()
group = asyncio.gather(*pending, return_exceptions=True)
loop.run_until_complete(group)
loop.close()
loop = asyncio.get_event_loop()
:取得事件迴圈例項。task = loop.create_task(main())
:將main()
協程包裝成一個任務並排程至事件迴圈。loop.run_until_complete(task)
:執行事件迴圈直到task
完成。- 後續程式碼處理剩餘任務並關閉迴圈,
asyncio.run()
內部也執行了這些操作。
graph LR B[B] E[E] A[取得事件迴圈] --> B{建立任務}; B --> C[排程任務]; C --> D[執行迴圈]; D --> E{處理剩餘任務}; E --> F[關閉迴圈];
圖表説明: 此流程圖展示了 asyncio 事件迴圈的基本運作流程。
融合阻塞與非阻塞:asyncio 執行器的妙用
在現實場景中,我們常常需要處理阻塞的程式碼。asyncio 提供了執行器,讓我們可以在非同步環境中安全地執行阻塞操作。
import time
import asyncio
async def main():
print(f'{time.ctime()} 您好!')
await asyncio.sleep(1.0)
print(f'{time.ctime()} 再見!')
def blocking():
time.sleep(0.5)
print(f"{time.ctime()} 來自執行緒的問候!")
loop = asyncio.get_event_loop()
task = loop.create_task(main())
loop.run_in_executor(None, blocking)
loop.run_until_complete(task)
# ... (後續的關閉處理與前例相同)
blocking()
函式中的 time.sleep(0.5)
是阻塞操作。透過 loop.run_in_executor(None, blocking)
,我們將其交給執行器在另一個執行緒中執行,避免阻塞事件迴圈。
sequenceDiagram participant Main participant EventLoop participant Executor Main->>EventLoop: 建立任務 (main) Main->>EventLoop: 提交 blocking 函式到執行器 Main->>EventLoop: 執行迴圈 activate EventLoop EventLoop-->>Executor: 執行 blocking 函式 activate Executor Executor->>Executor: 執行 time.sleep(0.5) Executor->>Executor: 列印 "來自執行緒的問候!" deactivate Executor EventLoop->>EventLoop: 執行 main 協程 EventLoop->>EventLoop: 執行 asyncio.sleep(1.0) EventLoop->>EventLoop: 列印 "您好!" 和 "再見!" deactivate EventLoop
圖表説明: 此循序圖展示了 main()
協程和 blocking()
函式在事件迴圈和執行器中的執行流程。
我認為,asyncio 的執行器機制巧妙地解決了在非同步程式設計中處理阻塞操作的難題,讓開發者能更靈活地運用現有程式碼,並兼顧效能與程式碼的可讀性。
透過以上範例和圖表,我希望您對 asyncio 事件迴圈及其核心概念有了更清晰的理解。在接下來的文章中,我將繼續探討 asyncio 的其他進階應用,帶您領略非同步程式設計的更多奧秘。
在網路應用開發中,有效管理 socket 通訊至關重要。Asyncio 提供了多元的工具來處理網路互動,其中 streams API
提供簡潔的 socket 通訊方式,適合快速建構網路應用原型。當原型趨於成熟,需要更精細的控制時,protocols API
則能派上用場。不過,多數專案初期使用 streams API
維持簡潔性通常更有效率,除非您已預見特定問題需要 protocols API
解決。當然,若使用 aiohttp 等與 Asyncio 相容的第三方函式庫,通常函式庫會自行處理 socket 通訊,您甚至無需直接操作 Asyncio 的網路層級,此時理解函式庫的檔案就變得格外重要。
Asyncio 函式庫旨在同時滿足終端使用者和框架開發者的需求,這也導致其 API 看起來較為龐雜。本文將提供清晰的指引,協助您快速找到所需的 API。
協程:Asyncio 的核心
協程、非同步函式等術語究竟代表什麼?以下範例將展示一些底層互動,雖然在一般程式中不常見,但能幫助您更清晰地理解 Asyncio 的基本組成,更容易掌握後續的內容。
所有範例都可在 Python 3.8 直譯器的互動模式下重現。我建議您親自輸入這些程式碼,觀察輸出結果,並嘗試不同的互動方式,以加深對 async
和 await
的理解。
Asyncio 最早於 Python 3.4 引入,但 async def
和 await
的新語法直到 3.5 才出現。在 3.4 版本中,人們使用特殊方式處理產生器,使其如同協程般運作。在一些較舊的程式碼函式庫中,您可能會看到使用 @asyncio.coroutine
裝飾,與包含 yield from
陳述式的產生器函式。使用 async def
建立的協程現在被稱為原生協程,因為它們在語言層面上就是協程。本文將聚焦於原生協程。
新關鍵字:async def
讓我們從最簡單的範例開始:
async def f():
return 123
print(type(f)) # 輸出:<class 'function'>
import inspect
print(inspect.iscoroutinefunction(f)) # 輸出:True
這段程式碼定義了一個最簡單的協程。它看似普通函式,只是以 async def
開頭。f
的型別並非「協程」,而是一個普通的函式。雖然通常將 async def
函式稱為協程,但嚴格來説,Python 將它們視為協程函式。這與 Python 中產生器函式的運作方式相同。即使有時被錯誤地稱為「產生器」,它仍然是一個函式,只有呼叫它時才會傳回產生器。協程函式的工作方式也完全相同:您需要呼叫 async def
函式才能獲得協程物件。inspect
模組提供了比內建函式 type()
更強大的自省能力。iscoroutinefunction()
函式可以區分普通函式和協程函式。
當我們呼叫 f()
時:
async def f():
return 123
coro = f()
print(type(coro)) # 輸出:<class 'coroutine'>
print(inspect.iscoroutine(coro)) # 輸出:True
呼叫 f()
產生一個協程物件 coro
,其型別為 coroutine
。協程是一個物件,它封裝了在完成之前暫停底層函式並還原其執行狀態的能力,這與產生器非常相似。
flowchart LR C[C] A[Call f] --> B[Create coroutine object coro] B --> C{Type is coroutine}
圖表説明: 這個流程圖展示了呼叫 f()
函式後建立協程物件 coro
的過程。
sequenceDiagram participant 主程式 participant 協程 f() 主程式->>協程 f(): 呼叫 f() activate 協程 f() 協程 f()->>主程式: 傳回協程物件 coro deactivate 協程 f()
圖表説明:此序列圖描述了主程式呼叫協程函式 f()
並傳回協程物件 coro
的互動過程。
在現今軟體開發領域,高效能的 I/O 操作至關重要。Python 的 asyncio
函式庫提供了一種優雅的非同步程式設計解決方案,讓開發者得以擺脫傳統同步程式設計的束縛,充分發揮硬體效能。本文將深入剖析 asyncio
的核心概念,並結合實務案例,引領讀者進入非同步程式設計的世界。
我發現許多開發者對於 asyncio
中的 Task
和 Future
物件感到困惑。因此,我會先釐清兩者的區別與關聯,再進一步探討事件迴圈的運作機制。
Task
與 Future
:非同步操作的根本
Future
物件代表一個非同步操作的最終結果,它像是一個承諾,保證在未來某個時間點會得到結果。而 Task
物件則是 Future
的子類別,專門用於包裝協程,使其能在事件迴圈中被排程和執行。
graph LR A[Future 物件] --> B(代表非同步操作的結果); C[Task 物件] --> D(包裝協程); D --> B;
上圖展示了 Future
和 Task
的關係。Future
是基底,代表非同步操作的結果,而 Task
則繼承自 Future
,專門用於管理協程的執行。
建立任務:asyncio.create_task()
asyncio.create_task()
函式是建立 Task
物件的主要方式。它接受一個協程作為引數,並傳回一個 Task
物件。
import asyncio
async def my_coroutine():
await asyncio.sleep(1)
return "Hello, asyncio!"
async def main():
task = asyncio.create_task(my_coroutine())
result = await task
print(result) # 輸出: Hello, asyncio!
asyncio.run(main())
asyncio.create_task(my_coroutine())
建立了一個 Task
物件,並將 my_coroutine()
協程包裝在其中。await task
會等待任務完成並取得結果。
執行器與 Future
物件
當使用執行器在另一個執行緒或行程中執行同步程式碼時,asyncio
會傳回一個 Future
物件,而非 Task
物件。
import asyncio
import time
def blocking_function():
time.sleep(1)
return "Result from blocking function"
async def main():
loop = asyncio.get_running_loop()
future = loop.run_in_executor(None, blocking_function)
result = await future
print(result) # 輸出: Result from blocking function
asyncio.run(main())
loop.run_in_executor(None, blocking_function)
將 blocking_function()
提交到執行器執行,並傳回一個 Future
物件。
事件迴圈:非同步程式設計的引擎
事件迴圈是 asyncio
的核心,它負責排程和執行協程、處理 I/O 事件以及管理 Future
和 Task
物件。
sequenceDiagram participant Client participant Event Loop participant Coroutine 1 participant Coroutine 2 Client->>Event Loop: 提交協程 activate Event Loop Event Loop->>Coroutine 1: 執行 activate Coroutine 1 Coroutine 1->>Event Loop: 等待 I/O deactivate Coroutine 1 Event Loop->>Coroutine 2: 執行 activate Coroutine 2 Coroutine 2->>Event Loop: 等待 I/O deactivate Coroutine 2 Event Loop->>Coroutine 1: I/O 完成,繼續執行 activate Coroutine 1 Coroutine 1->>Event Loop: 完成 deactivate Coroutine 1 Event Loop->>Coroutine 2: I/O 完成,繼續執行 activate Coroutine 2 Coroutine 2->>Event Loop: 完成 deactivate Coroutine 2 deactivate Event Loop
上圖展示了事件迴圈如何排程和執行多個協程。當一個協程遇到 I/O 操作時,它會將控制權交還給事件迴圈,讓其他協程得以執行。
ensure_future()
的迷思
過去,asyncio.ensure_future()
常被用於建立 Task
物件。然而,我建議直接使用 asyncio.create_task()
,因為它更清晰易懂,與避免了 ensure_future()
可能造成的混淆。
asyncio
提供了強大的工具,讓 Python 開發者能夠更有效率地處理 I/O 密集型任務。理解 Future
、Task
和事件迴圈的運作機制,是掌握非同步程式設計的關鍵。透過本文的解析和實務案例,希望能幫助讀者更深入地理解 asyncio
,並在實際專案中發揮其效能優勢。