我們本節(jié)即將學習的 Python asyncio 包亚铁,使用基于事件循環(huán)驅動的協(xié)程實現(xiàn)并發(fā)座菠。這是 Python 中最大抖韩,也是最具雄心壯志的庫之一漫玄。
既然 asyncio 基于事件驅動劈愚,那么讓我們首先來了解下事件驅動編程瞳遍,再進入正題。
一. 事件驅動
1.1 單線程菌羽、多進程以及事件驅動編程模型的比較
事件驅動編程是一種編程范式掠械,程序的執(zhí)行流程由外部事件來決定。它的特點是包含一個事件循環(huán)注祖,當外部事件發(fā)生時猾蒂,使用一種回調機制來觸發(fā)相應的處理。
此外是晨,單線程同步以及多進程也是常見的編程范式肚菠。下圖對比了單線程、多線程以及事件驅動編程模型罩缴。
上圖中蚊逢,這個程序有 A / B / C
個任務需要完成层扶,每個任務在執(zhí)行過程中都存在 IO 阻塞,阻塞的時間使用黑色塊表示烙荷。
單線程同步模型:多個任務按序執(zhí)行镜会。一旦某個任務因為 I/O 而阻塞,其他所有的任務都必須等待终抽,直到前面的任務完成之后它們才能依次執(zhí)行戳表。即使任務之間并沒有互相依賴,仍然需要等待昼伴,使得程序不必要的降低了運行速度匾旭。
多進程同步模型中:各個任務分別在獨立的進程中執(zhí)行。進程由操作系統(tǒng)來管理圃郊,在多處理器系統(tǒng)上可以并行處理季率,或者在單處理器系統(tǒng)上交錯執(zhí)行。與單線程同步程序相比描沟,多進程的效率更高飒泻,但同時創(chuàng)建進程的資源消耗也比較大。
多線程操作共享資源時吏廉,還需要考慮同步互斥機制泞遗,而且 CPython 解釋器無法利用計算機多核的特性。
事件驅動編程模型中:多個任務在一個單獨的線程中交錯執(zhí)行席覆。當遇到 I/O 操作時史辙,注冊一個回調到事件循環(huán)中,然后當 I/O 操作完成時繼續(xù)執(zhí)行佩伤。
事件循環(huán)輪詢所有的事件聊倔,當事件到來時將它們分配給等待處理事件的回調函數(shù)。因此生巡,一個任務在遇到 IO 阻塞時耙蔑,可以讓步出 CPU 的使用權,讓其它任務繼續(xù)執(zhí)行孤荣,而不是一直等待甸陌。事件驅動編程模型不需要關心線程安全問題。
我們之前介紹的 IO 多路復用盐股,使用的就是事件驅動編程模型钱豁,利用 select / poll / epoll
將 IO 事件交給系統(tǒng)內核監(jiān)控,當某個 IO
描述符結束阻塞準備就緒時疯汁,就將其返回牲尺。
1.2 協(xié)程的引入
事件驅動編程模型有諸多好處,但在嵌套多層回調時幌蚊,可讀性較差谤碳,出現(xiàn)異常排查也很困難凛澎,非常不利于后期的維護。
于是估蹄,我們引入?yún)f(xié)程來解決上面的問題塑煎,允許我們 采用同步的方式去編寫異步的代碼,使代碼的可讀性提升臭蚁,既操作簡單最铁,速度又快。
協(xié)程使用單線程去切換任務垮兑,性能遠高于線程切換冷尉,且不需要加鎖,并發(fā)性高系枪。
進程雀哨、線程以及協(xié)程的關系可以使用下圖描述:
進程可以包含多個線程,多個線程共享進程的資源私爷,因此線程比進程更輕量雾棺;而協(xié)程的本質是一個函數(shù),一個線程可以包含多個協(xié)程衬浑,協(xié)程比線程更輕量捌浩。
1.3 相關概念
- 并發(fā):CPU 在多個任務之間不斷切換,比如在一秒內 CPU 切換了 100 個進程工秩,就可以認為 CPU 的并發(fā)是 100尸饺。
- 并行:在多核 CPU 中,多個任務在不同的 CPU 上同時運行助币;并行數(shù)量和 CPU 數(shù)量是一致的浪听。
- 同步:必須等待前一個調用完成后,再開始新的的調用眉菱。
- 異步:不必等待前一個操作的完成迹栓,就開始新的的調用。
- 阻塞:調用函數(shù)的時候倍谜,當前線程被掛起迈螟。
- 非阻塞:調用函數(shù)的時候,當前線程不會被掛起尔崔,而是立即返回結果(不管什么樣的結果)。
二. asyncio 模塊
Python3.4 中引入 asyncio 模塊褥民,創(chuàng)建協(xié)程函數(shù)時使用@asyncio.coroutine
裝飾器裝飾季春。
我們前面介紹的
yield from
是python3.4
前的用法,即包含yield from
語句的函數(shù)即可作為生成器函數(shù)消返,也可以稱作協(xié)程函數(shù)载弄。
Python3.4 之后耘拇,使用 @asyncio.coroutine
裝飾的函數(shù)即可稱作協(xié)程函數(shù)。關于 asyncio
中的基本概念總結如下:
術語 | 說明 |
---|---|
coroutine 協(xié)程對象 |
使用 @asyncio.coroutine 裝飾器裝飾的函數(shù)被稱作協(xié)程函數(shù)宇攻,它的調用不會立即執(zhí)行惫叛,而是返回一個協(xié)程對象。協(xié)程對象需要包裝成任務注入到事件循環(huán)逞刷,由事件循環(huán)調用嘉涌。 |
task 任務 |
使用協(xié)程對象作為參數(shù)創(chuàng)建任務,任務是協(xié)程對象的進一步封裝夸浅,其包含任務的各種狀態(tài) |
event_loop 事件循環(huán) |
協(xié)程函數(shù)必須添加到事件循環(huán)中仑最,由事件循環(huán)去運行,因為直接調用協(xié)程函數(shù)返回的是協(xié)程對象帆喇,協(xié)程函數(shù)并不會真正開始運行警医。事件循環(huán)控制任務運行流程,是任務的調用方坯钦。 |
示例 asyncio 實現(xiàn)協(xié)程的簡單示例
import time
import asyncio
@asyncio.coroutine
def do_some_work():
print('Coroutine Start.')
time.sleep(3) # 模擬IO操作
print('Print in coroutine.')
def main():
start = time.time()
loop = asyncio.get_event_loop()
coroutine = do_some_work()
loop.run_until_complete(coroutine)
end = time.time()
print('運行耗時:{:.2f}'.format(end - start)) # 打印程序運行耗時
if __name__ == '__main__':
main()
運行結果:首先使用協(xié)程裝飾器 @asyncio.coroutine
創(chuàng)建協(xié)程函數(shù)预皇,協(xié)程函數(shù)中使用 time.sleep(3)
模擬一個耗時的IO操作俊柔。
asyncio.get_event_loop()
用來創(chuàng)建事件循環(huán)此迅;每個線程中只能有一個事件循環(huán)废麻,get_event_loop
獲取當前已經(jīng)存在的事件循環(huán)约巷,如果當前線程中沒有谣膳,則新建一個事件循環(huán)并蝗。
loop.run_until_complete(coroutine)
將協(xié)程對象注入到事件循環(huán)匹中,協(xié)程的運行由事件循環(huán)控制帘瞭。事件循環(huán)的 run_until_complete
方法會阻塞運行洋丐,直到任務全部完成呈昔。
協(xié)程對象作為 run_until_complete
方法的參數(shù),loop
會自動將協(xié)程對象包裝成任務來運行友绝。下節(jié)我們會講到多個任務注入事件循環(huán)的情況堤尾。