【Python - asyncio】非同步 I/O 簡介

2024/01/23閱讀時間約 6 分鐘
raw-image


為什麼需要非同步? 我們在「【Web微知識系列】 Web Workers」有介紹到在瀏覽器可執行腳本Javascript環境底下如何完成非同步的操作, 主要是為了讓任務更有效率的進行, 不會因為一個非常耗時的工作堵塞住整個服務, 導致無法服務他人的窘境。


大家應該經常在餐廳裡會看到服務員協助顧客點餐吧! 而服務員的工作除了送餐、清潔之外還要協助顧客點餐, 那麼當每位顧客都花費10分鐘在思考要點什麼時, 服務員若在原地等待是一個非常浪費資源的行為, 因此通常每位服務員都先將顧客帶到位子上之後, 說明餐點跟給予菜單之後, 便可以釋出資源去服務其他客人, 甚至能夠完成其他工作, 這就是一個在我們生活中常常出現的非同步情境。


而Python在3.4版之後就有 asyncio 模組了, 但相關的API函數尚未完全, 因此使用起來並非那麼直觀, 而在3.8之後的功能就加入了許多語法糖封裝, 讓我們能夠很簡易的使用這些功能完成我們的非同步應用場景。


開發程式的過程中, 相信除了本身程式的運算之外, 更多的是網路的傳輸(外部API)、硬碟的存取(I/O), 而這些外部的服務都不應該阻塞我們的程式, 因此我們可以透過 asyncio 來做一個任務委外的動作, 而程式本身的運算也能夠持續執行, 這就是非同步的魅力所在。


概念建立之後, 我們就可以開始進行簡易的實作囉! 就讓我們一同探索非同步的 asyncio 吧!


協程 (Coroutine)

協程是一個比較特殊的函數, 也是為了解決單執行緒應用之中的等待浪費資源問題, 與常規函數不同,協程可以在適當的時機暫停、恢復和交互執行,這種能力使得協程特別適合處理非同步的任務,比如 I/O 操作、網絡請求等需要等待的工作。


async與await

這兩個語法糖主要是讓我們更直觀的標示異步的函式與執行的進入點。

  • async:用來宣告函數能夠有異步的功能。
  • await:用來標記異步的執行。

# async 宣告一個非同步函式
async def do(...):
...


...
...
# await 將控制權交給事件循環並等待結果回傳
await do(...)


這些語法想必常常在異步的Python程式中隨處所見吧, 這也是整個 asyncio 用來標示非同步函式的主要標誌。


關於事件循環

raw-image

回顧「【Web微知識系列】 Web Workers」 我們有提到事件循環的概念, 我們還是以餐廳服務員為例, 當我們收到客人的菜單時, 會將菜單交給內場的夥伴們進行料理, 而服務員就可以趁著備料的時候進行其他工作項目(擦桌子、帶位…), 直到料理完成之後, 切換回來將美味的料理交給客人享用, 這整個餐廳的日常工作就像是一個事件循環一般。


那在 asyncio 的世界裡應該怎麼使用呢? 我們可以透過「asyncio.get_event_loop」來建立一個事件循環, 然後再將上述的 「async func」帶入, 直到事件循環結束, 如下:


import asyncio

async def job():
...
await ...

asyncio.run(job())


多個非同步任務Tasks

「asyncio.create_task( )」 相信這個函式我們常常在原始碼中會常常看到, 而它就是一個更大的非同步單元, 因為我們都知道 await 是一個等待的概念

我們來看底下的例子, 共會需要3秒來完成整個事件,那這樣其實沒有得到非同步應用太大的好處, 我們是希望兩個echo之間是能夠在空閒時切換進行, 節省整體耗時。


import asyncio
import time
async def echo(msg, delay):
await asyncio.sleep(delay)
print(msg)
async def main():
start_time = time.time()
await echo('此作業需要1秒', 1)
await echo('此作業需要2秒', 2)
end_time = time.time()
execution_time = end_time - start_time
print(f'共計 {execution_time} 秒')
asyncio.run(main())


# python test.py
# 此作業需要1秒
# 此作業需要2秒
# 共計 3.003565788269043 秒


因此我們可以針對此情境進行優化, 透過任務的概念, 將兩個任務加入事件循環的Loop, 兩者互不堵塞, 讓任務執行最佳化, 我們可以看到共耗時約2秒左右。


import asyncio
import time
async def echo(msg, delay):
await asyncio.sleep(delay)
print(msg)

async def main():
start_time = time.time()
task1 = asyncio.create_task(
echo('此作業需要1秒', 1))
task2 = asyncio.create_task(
echo('此作業需要2秒', 2))
await task1
await task2
end_time = time.time()
execution_time = end_time - start_time
print(f'共計 {execution_time} 秒')
asyncio.run(main())

# python test.py
# 此作業需要1秒
# 此作業需要2秒
# 共計 2.0014331340789795 秒


結語

本章節主要是簡單的介紹一下Python的非同步概念, 它的精華之處在於讓我們能夠善用每個時刻不浪費, 既然程式語言能夠有這樣的概念, 我們的人生是否就像個事件循環一般呢? 我們是否也能善用這樣的技巧來讓我們的人生更加的精彩且有效率呢?


接下來我們會持續對於非同步協程的部份進行詳細的探討, 讓我們善用協程開發出高效的應用軟體吧!

91會員
260內容數
哈囉,我是阿Han,是一位 👩‍💻 軟體研發工程師,喜歡閱讀、學習、撰寫文章及教學,擅長以圖代文,化繁為簡,除了幫助自己釐清思路之外,也希望藉由圖解的方式幫助大家共同學習,甚至手把手帶您設計出高品質的軟體產品。
留言0
查看全部
發表第一個留言支持創作者!