python非同步程式設計之asyncio初識

金色旭光發表於2023-12-29

image

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

程式碼邏輯:

  1. 建立一個事件迴圈
  2. 將兩個非同步函式coro1,coro2封裝成兩個任務task1,task2
  3. 用asyncio.gather將兩個任務組合到一起,併發執行task1,task2
  4. 先執行task1,遇到IO切換到task2
  5. 執行task2,遇到IO切換,但此時沒有等待執行的任務,cpu為空
  6. task2執行完成,task1執行完成

從示例程式碼可以看出,協程的幾個關鍵要素:

  1. 事件迴圈
  2. 協程函式定義
  3. 可等待物件
  4. 併發執行

協程基本原理

組成協程最重要的因素就是事件迴圈任務

  • 任務就是一個物件,包括執行的程式碼,執行完成、失敗等狀態以及返回結果,任務中通常會有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完成再執行的耗時。

為什麼協程在IO密集時效能較好

很多人可能會疑問,多執行緒遇到IO也會切換,為什麼協程比執行緒效能好呢?
簡單來是三點:

  1. 協程更輕量級,切換需要恢復的上線文很少,所以比執行緒更快速
  2. 執行緒切換CPU是搶佔的,協程是主動讓出的,協程對CPU的使用更充分
  3. 協程更輕量級,啟動執行緒需要的記憶體資源比協程更多

示例程式碼的高階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)

連載一系列關於python非同步程式設計的文章。包括同非同步框架效能對比、非同步事情驅動原理等。歡迎關注微信公眾號第一時間接收文章。

相關文章