async await介紹
用asyncio提供的@asyncio.coroutine
可以把一個生成器標記爲協程類型,然後在協程內部用yield from 等待IO操作,讓出cpu執行權。
然而異步的關鍵字yield 和 yield from畢竟是複用生成器關鍵字,兩者在概念上糾纏不清,所以從Python 3.5開始引入了新的語法async和await替換yield 和 yield from,讓協程的代碼更易懂。
簡單來說,可以這樣理解:
- async 替換
@asyncio.coroutine
:標識一個函數爲異步函數 - await 替換 yield from:標識等待IO操作,讓出CPU執行權
async 實現協程示例
由於協程在各個python版本中有細微差異,本篇以python3.10爲例
import asyncio
async def coro1():
print("start coro1")
await asyncio.sleep(2)
print("end coro1")
async def coro2():
print("start coro2")
await asyncio.sleep(1)
print("end coro2")
# 創建事件循環
loop = asyncio.get_event_loop()
# 創建任務
task1 = loop.create_task(coro1())
task2 = loop.create_task(coro2())
# 運行協程
loop.run_until_complete(asyncio.gather(task1, task2))
# 關閉事件循環
loop.close()
輸出結果:
start coro1
start coro2
end coro2
end coro1
代碼邏輯:
- 創建一個事件循環
- 將兩個異步函數coro1,coro2封裝成兩個任務task1,task2
- 用asyncio.gather將兩個任務組合到一起,併發執行task1,task2
- 先執行task1,遇到IO切換到task2
- 執行task2,遇到IO切換,但此時沒有等待執行的任務,cpu爲空
- task2執行完成,task1執行完成
從示例代碼可以看出,協程的幾個關鍵要素:
- 事件循環
- 協程函數定義
- 可等待對象
- 併發執行
協程基本原理
組成協程最重要的因素就是事件循環和任務。
- 任務就是一個對象,包括執行的代碼,執行完成、失敗等狀態以及返回結果,任務中通常會有IO切換。
- 事件循環,可以把它當做是一個while循環。while循環在週期性的運行並執行一些任務,所有任務執行完成會關閉循環。
僞代碼示例如下:
任務列表 = [ 任務1, 任務2, 任務3,... ]
while True:
可執行的任務列表,已完成的任務列表 = 去任務列表中檢查所有的任務,將'可執行'和'已完成'的任務返回
for 就緒任務 in 已準備就緒的任務列表:
執行已就緒的任務
for 已完成的任務 in 已完成的任務列表:
在任務列表中移除 已完成的任務
如果 任務列表 中的任務都已完成,則終止循環
獲取和創建事件循環:loop = asyncio.get_event_loop()
驅動事件循環運行:loop.run_until_complete(asyncio.gather(task1, task2))
事件循環過程:
事件循環中執行任務,當執行到某一個任務時遇到IO時,協程會讓出CPU給第二個任務執行,第二個任務中遇到IO再次讓出CPU,直到所有任務完成。這就是協程併發性能好的一個關鍵能力:遇到IO切換任務執行,避免了程序等待IO完成再執行的耗時。
示例代碼的高級api實現
示例代碼中使用了asyncio.get_event_loop()
和 loop.run_until_complete()
等代碼,這些其實asyncio包的低級API,是爲了展示底層原理而使用的。通常更推薦高級APIasyncio.run()
實現協程併發。
import asyncio
async def coro1():
print("start coro1")
await asyncio.sleep(2)
print("end coro1")
async def coro2():
print("start coro2")
await asyncio.sleep(1)
print("end coro2")
async def main():
task1 = asyncio.create_task(coro1())
task2 = asyncio.create_task(coro2())
await asyncio.gather(task1, task2)
asyncio.run(main())
run() 從功能上等價於以下低階API
loop = asyncio.get_event_loop()
task = loop.create_task(coro())
loop.run_until_complete(task)
爲什麼協程在IO密集時性能較好
很多人可能會疑問,多線程遇到IO也會切換,爲什麼協程比線程性能好呢?
簡單來是三點:
- 協程更輕量級,切換需要恢復的上下文很少,所以比線程更快速
- 線程切換CPU是搶佔的,協程是主動讓出的,協程對CPU的使用更充分
- 協程更輕量級,啓動線程需要的內存資源比協程更多
連載一系列關於python異步編程的文章。包括同異步框架性能對比、異步事情驅動原理等。歡迎關注微信公衆號第一時間接收推送的文章。