Python 的 asyncio 庫專為高效非同步程式設計而設計:
import asyncio |
透過使用 async def 定義函式並使用 await 和 asyncio.sleep(1),我們建立了可以暫停和恢復的 coroutines,允許程式執行其他任務,而不是空閒等待。
這種方法尤其適用於 IO 繫結任務,如 API 呼叫,在這種情況下,事件迴圈可以併發管理多個任務,而不會阻塞。
我們呼叫 asyncio.run(run()),將我們的主函式啟動到事件迴圈中。這正是 asyncio 的優勢所在,它可以無縫地協調眾多 coroutines 的執行。
讓我們嘗試一下 asyncio 中的常見模式。讓我們建立一堆作業,將它們放在事件迴圈中,等待它們完成。
記憶體飢餓事件迴圈
# ... mock_api_request |
- 我們從 mock_api_request 例程中建立一個任務。
- 我們將所有任務新增到列表任務中。(如果有一百萬個專案,可能會耗盡記憶體)
- 如果記憶體沒有耗盡,Python 將在呼叫 await asyncio.wait(tasks) 時開始工作。
- 每個任務都會進入事件迴圈。事件迴圈抓取一個任務並開始工作。
- Python 將同步執行任務,直到看到 await 關鍵字。
- 一旦看到 await 關鍵字,事件迴圈就會放棄,並切換到佇列中的下一個任務。
- 重複第 5 步和第 6 步,偶爾檢查等待的任務是否完成。
- 當等待的函式完成 IO 工作後,它將繼續執行,直到下一個等待或函式結束。
- 這樣一直持續到列表中的每個專案都完成為止。
酷,這就是事件迴圈的大致作用。
那麼,我們如何加快同步-非同步程式的執行速度呢?
Batching
# ... mock_api_request |
至少在記憶體使用方面,這要好得多。不過,速度不一定比memory-inefficient的版本快。
你會注意到,在分批執行時,每次等待 asyncio.wait(tasks),你都必須等最後一個任務完成後才能開始下一批。這可不行。
生產者/消費者模式
在下一次迭代中,我們將引入 asyncio.Queue()。
# ... mock_api_request |
當我們執行這個程式時,我們會注意到事情或多或少又恢復了同步。現在的情況是,我們透過生產者將所有專案放入佇列,但消費者每次只抓取佇列中的一個專案。
讓我們把生產者/消費者模式與之前的批處理版本結合起來。
多個消費者
讓我們再次修改執行函式。
async def run(): |
現在我們建立一個列表,並將其分配給變數消費者。然後,我們建立 10 個 asyncio 任務,等待生產者和所有 10 個消費者完成工作。現在,只要你執行它,消費者就會隨時從佇列中拉出,你就能獲得源源不斷的工作!
當生產者完成工作,佇列為空時,消費者就會掛起,等待永遠不會到來的任務。程序永遠不會停止。如果你的生產者連線的是一個永遠不會結束的源,那麼這種情況可能還不錯。為此,我們需要某種方法來告訴消費者退出。
使用 `None` 傳送訊號
async def consumer(queue: asyncio.Queue): |
我們在消費者中新增了一個條件,用於檢查佇列中是否傳遞了 None。我們可以將此作為退出的訊號,因為通常我們可以假設專案不應該是 None。
我們還應該更新生產者以傳送此訊號。
async def producer(queue: asyncio.Queue, number_of_consumers: int): |
請注意我們是如何在生產者的函式簽名中新增 number_of_consumers 的。我們需要在佇列中放入與 Worker 數量相同的 None,因為每個 None 都需要自己的訊號。只要記住在執行方法中更新生產者任務即可。
producer_task = asyncio.create_task(producer(queue, number_of_consumers))
有時,"None "是一個有效值,或者您需要更復雜的訊號。Asyncio 可以滿足您的需求。
Asyncio 事件
讓我們新增一個 asyncio.Event() 變數,用作我們的訊號。
async def run(): |
我們還需要更新消費者和生產者。
async def producer(queue: asyncio.Queue, stop_event: asyncio.Event): |
如果您使用 number_of_consumers 在佇列中放入了大量 None 值,則需要從生產者的函式簽名中刪除該值。
在消費者中,重要的是 if stop event.is_set() 和 queue.empty():。這將確保在實際退出之前首先完成所有任務。這可能會根據你的需要而有所不同。例如,您可能需要一個單獨的 asyncio.Event 用於在不耗盡佇列的情況下終止函式。
總結
現在您應該掌握了在 Python 中最佳化和提高 IO 繫結呼叫吞吐量的最佳化路徑和各種模式。
您還可以做一些事情來提高速度。例如,您可以改變消費者的數量。您還可以將 "佇列 "部分解除安裝到專用佇列(如 RabbitMQ 或 NATS),這樣您就可以擴充套件和/或分離生產者和消費者。