什麼是 asyncio
asyncio 是 Python 內建的module,在 Python 3.4 時加入
是一種單 thread 的設計,它靠著 cooperative multitasking (協同運作式多工) 讓我們能多個工作併發處理 (concurrent)
協同運作式多工相對於搶佔式多工(Preemptive multitasking),協作式多工要求每一個運行中的程式,定時放棄自己的執行權利,告知作業系統可讓下一個程式執行
多線程採用的是搶佔式多工,多線程是由作業系統做排程,線程執行任務途中會被外力(作業系統)中斷改排其他線程執行,而協同運作式多工不由作業系統排程,在任務執行時遇到需要等待回應的狀況,會放棄執行權,改執行別的任務,而原任務在等到回應後再繼續執行
這篇文章主要是大概介紹 asyncio是如何做到 cooperative multitasking
在介紹 asyncio 是怎麼做到 cooperative multitasking,首先需要知道什麼是Coroutine與Event Loop
什麼是Coroutine (協程)
在介紹 Corotines 之前,如果先知道 Python 的 Generator 或許比較好理解,因為兩者的概念其實相當類似
認識 Python Generator 首先要知道 yield 這個語法用意
直接看範例比較好理解用途
def genNumber():
x = 0
for _ in range(3):
yield x
x = x + 1
return 5
for n in genNumber():
print(n) # 會印出 0 1 2
z = genNumber()
print(z) # 印出 generator object genNumber at 0x01CCDB18
print(next(z)) # 印出 0
print(next(z)) # 印出 1
print(next(z)) # 印出 2
try:
print(next(z)) # 會 raise StopIteration
except StopIteration as e:
print(e.value) # 印出 5
上面的範例,有一點需要先注意的是 z = genNumer() 這行其實沒有運行我們寫的程式碼,它只是實例化一個 generator object (所以 print(z) 是印出 <generator object ...>)
genNumber() 是 generator object,當跑這物件的 __next__() 時才會真的執行我們寫的程式碼 ( next(obj)會跑obj物件的__next__() 函式 )
而每一次的執行如果遇到 yield,會停在 yield 這行,並回傳 yield 後帶的值,且下次的 __next__() 會又接續上次的斷點繼續執行,直到程式碼跑完這時就會 raise StopIteration
那為什麼說 Generator 跟 Coroutine 有關呢?
看看下面的例子
async def func():
return 1
coro = func()
print(coro) # 印出 coroutine object func at 0x01C9E6E8
try:
coro.send(None)
except StopIteration as e:
print(e.value) # 印出 1
不覺得有點既視感嗎?
Coroutine 的行為其實可以類比 Generator
總結來說Corotine同Generator有這幾個特性
- 函式可以暫停,並且保存當前運行狀態,恢復時能從保存狀態的地方執行
- 可以向暫停的地方傳入值,如此可以做到多個任務間的傳遞
另外要跟 Coroutine 互動,除了使用 send() 方法還有 throw(),這是讓 Coroutine 執行噴錯
import asyncio
async def f():
try:
while True: await asyncio.sleep(0)
except asyncio.CancelledError:
print('cancel')
else:
return 111
coro = f()
coro.send(None)
coro.throw(asyncio.CancelledError) # 印出cancel後raise StopIteration
async 與 await 語法
async 關鍵字是用來宣告函式為 coroutine 用
await 後必須接 awaitable 的物件,awaitable 的物件有 Coroutine 和有實作__await__()方法的物件,當執行到 await 會將控制權還回 event loop,並等待到回傳值後再繼續向下執行
Event Loop
Event loop 包裝了一些方法讓我們更方便跟 Coroutine 互動,在要處理多個 Coroutine 運行也較為簡單
Event loop 有兩種
- SelectorEventLoop:使用selectors module
- ProcatorEventLoop:for Windows, 使用 I/O Completion Ports (IOCP)
async def f():
return 1
loop = asyncio.get_event_loop()
coro = f()
loop.run_until_complete(coro) # 會有印出 1
像上例,在run_until_complete()其實內部就幫我們處理了coroutine send()與catch StopIteration
Task 與 Future
asyncio 模組裡的 Task 物件封裝 Coroutine,便於我們控制執行
Future 物件則是包裝執行狀態與結果
Futute 有點難解釋,但從 Future 物件方法來看應該能大致理解 Future 的應用
- set_result(result):使 Future done,並設定 result 值,若Future已經done還 set_result 會有 InvalidStateError
- set_exception(exception):使 Future done,並設定exception,若Future已經done還 set_exception會有 InvalidStateError
- cancel(msg=None):cancel Future
- result():若Future done,回傳Future result值,但若是因為set_cxception(exception)才 done的話會 raise exception,若Future被cancelled,則會有 CancelledError,若Future還沒done的話(沒set_result)則有InvalidStateError
- done():若Future done 回傳True (cancel狀態也會是True)
- cancelled():若Future cancelled 回傳 True
- add_done_callback():設定當Future done時要跑的callback
- get_loop():回傳 Funture 綁定的 event loop
另外 Task 是繼承 Future 的,Task 有點像是 Coroutine 加上 Future 物件方法的感覺
asyncio 運作
有了上面的 Coroutine 與 Event loop 的認知後,在來看 asyncio 的運作,是如何做到cooperative multitasking (協同運作式多工)
asyncio 名字是指 async I/O (異步I/O),async 意指不會阻塞當前的程式執行,在 asyncio 模組裡除了 Event loop 與 Coroutine 還有包裝 async I/O 方法 (https://docs.python.org/zh-tw/3/library/asyncio-stream.html#asyncio-streams),Event loop 裡則使用了 selectors module 來做到能在 async I/O 執行完時做喚醒的動作
然後協程的特性是可以保存執行斷點,恢復時能繼續執行,加上協程這設計,當協程跑到 async I/O 可以先斷點改跑其他的協程,到所有協程處於waiting,Event loop 會call select函式並等待async I/O 完成的通知,當I/O有資料可以讀取select 會回傳對應的socket,asyncio 會將綁定這I/O socket的Future設定成done,之前斷點的協程在這之前有使用add_done_callback()加到這個Future裡,當這Future狀態變成 done 就會 call 之前的協程,這就相當於喚醒之前執行到中途的協程繼續跑
如此搭配下,Event Loop單線程就可以在多個協程下交錯運行,且不會被耗時的I/O給阻塞住
(附註,asyncio裡內建的異步I/O方法相當底層,使用上較為困難,這時候可以使用其他第三方async函式庫,像是 aiohttp (https://docs.aiohttp.org/en/stable/) 就將asyncio的異步方法包裝讓我們更簡單做到 async 的 http 操作)
參考資料 / 推薦閱讀
1. https://stackoverflow.com/questions/49005651/how-does-asyncio-actually-work
沒有留言:
張貼留言