Python加速技巧|異步處理(Asynchronous)

Python加速技巧|異步處理(Asynchronous)
Photo by Ashwini Chaudhary(Monty) / Unsplash

在一開始開發程式時,「自動」執行批次任務相對比「手動」下,怎麼樣都覺得很有效率,但當程式開發到一定程度時,某些專案就會開始要求速度,例如我近幾個月都在開發選擇權造市程式,這個時候執行速度就是相當重要啦!

一般正常狀態撰寫的程式是由上而下的線性執行,當執行無誤完成第一步後,才會接著執行下一步。如果任務之間沒有相依性,我們可以如何增加效率呢?

我們先舉一個生活上的例子——「煮飯」,近期作為外派菜鳥的我正在學習如何自己煮飯,畢竟在台灣的都會區生活實在太方便了,到了異國只能自己來才吃得到想吃的味道。而我目前的階段屬於煮飯菜鳥,一次不能做太多事,免得手忙腳亂全部搞砸,所以一次只能做一件事情,因為很多事情都沒做過也不知道時間處理會如何。

Lv 1 廚房菜鳥:(Single thread)

  • 一次處理一件任務:例如先洗菜,再煮一鍋水,再把菜放下去燙;洗米、泡米、再放入電鍋煮。
  • 假設每件任務都是獨立且依序執行

Lv 2 廚房進階菜鳥:(Asynchronous)

  • 區別什麼任務會花比較久的時間,但又不用自己實際盯場,例如電鍋煮飯。
  • 當在執行等待任務完成的時間,就可以先去做其他任務,例如洗完米按下電鍋後,就可以來準備煎荷包蛋或是其餘備料,等飯煮好時,再回來處理飯的其他流程。

Lv 3 廚房老手:(Multiple threads & Asynchronous)

  • 可以區分什麼任務會花比較久的等待時間。
  • 可同時處理多項任務,例如可以同時煮麵跟煎荷包蛋,但必須要有多個瓦斯爐或鍋子。
😂 這些都是我在煮飯時悟出的程式執行道理!

回到Python的世界,我們先使用廚房菜鳥的模式,我們的範例是煮咖啡與烤貝果,如下面的範例,煮咖啡要3分鐘,而烤貝果則需要5分鐘,如果我們一項接著一項做,總共需要8分鐘左右才能吃到一個早餐組合。

一般模式(非使用Async):

import time

def brew_coffee():
    print("Start brew coffee")
    time.sleep(3 * 60)
    print("End brew coffee")
    return "coffee ready"
    
def toast_bagel():
    print("Start toast bagel")
    time.sleep(5 * 60)
    print("End toast bagel")
    return "bagel toasted"
    
def main():
    start_time = time.time()
    
    result_coffee = brew_coffee()
    result_bagel = toast_bagel()
    
    end_time = time.time()
    elapsed_time = end_time - start_time
    
    print(f"Result of brew_coffee: {result_coffee}")
    print(f"Result of toast bagel: {result_bagel}")
    print(f"Total execution time: {elapsed_time:2f} seconds")

if __name__ == "__main__":
    main()

第二種模式比較屬於正常人的模式,我們知道咖啡機可能要花一些時間預備,所以啟動後,這時候我們也趕快把貝果從冰箱拿出來放到烤箱,並將時間設定5分鐘,就讓烤箱開始烤,而咖啡機這時候可能也差不多好了,再回到咖啡機去處理磨豆等任務,當烤箱叮的一聲完成後,咖啡與烤貝果等任務其實也完成了,這個就是Async的概念。

Async模式

import asyncio
import time

async def brew_coffee():
    print("Start brew coffee")
    await asyncio.sleep(3 * 60)
    print("End brew coffee")
    return "coffee ready"
    
async def toast_bagel():
    print("Start toast bagel")
    await asyncio.sleep(5 * 60)
    print("End toast bagel")
    return "bagel toasted"
    
async def main():
    start_time = time.time()
    
    # method 1: asyncio.gather()
    batch = asyncio.gather(brew_coffee(), toast_bagel())
    result_coffee, result_bagel = await batch
    
    # method 2: asyncio.create_task()
    # coffee_task = asyncio.create_task(brew_coffee())
    # toast_task = asyncio.create_task(toast_bagel())
    
    # result_coffee = await coffee_task
    # result_toast = await toast_task
    
    end_time = time.time()
    elapsed_time = end_time - start_time
    
    print(f"Result of brew_coffee: {result_coffee}")
    print(f"Result of toast bagel: {result_bagel}")
    print(f"Total execution time: {elapsed_time:2f} seconds")
    
if __name__ == "__main__":
    asyncio.run(main())

Asynchronous是近年比較常使用的加速處理模式,主要用來加速多項耗時較長的任務,例如資料庫I/O操作與爬蟲等任務,而這兩項任務都是等待其他服務的回應,而非本機端在進行處理。所以想透過Async加速的第一步就是識別專案的任務有哪些,個別分析耗時的狀況。

另一個系列加速處理的就是multiprocessing和threading,這兩項屬於平行同步處理,可能會導致一些資源消耗或併發一些問題,例如瞬間CPU達到極限或是記憶體佔用率過高,使得全部程序都停擺,而我自己的選擇權造市則使用混合模式執行。


Reference

  1. Socratica - AsyncIO, await, and async - Concurrency in Python
  1. MyApollo - Python asyncio從不會到上路
Python asyncio 從不會到上路
自從 Python 3.4 推出 asyncio 模組之後,開發者在提升 Python 程式效能的解決方案上又多了 1 種選擇。 不過相較於較為人所熟知的 multiprocessing 與 threading 而言,大多數初學者並不習慣非同步式(asynchronous)式的開發思維,但只要能夠掌握 asyncio 模組中幾點重要的概念,即使是從未接觸過的初學者,也能夠慢慢掌握 asyncio 的使用方式。 本文將重點介紹 asyncio 模組中的重要概念,並透過實際範例理解 asyncio 的運作,從而學會如何使用 asyncio 模組。