2021年11月28日 星期日

[Python] asycio工作原理淺談

 

什麼是 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



沒有留言:

張貼留言