python教程:使用 async 和 await 協程進行併發程式設計

和牛發表於2020-04-20

python 一直在進行併發程式設計的優化, 比較熟知的是使用 thread 模組多執行緒和 multiprocessing 多程式,後來慢慢引入基於 yield 關鍵字的協程。 而近幾個版本,python 對於協程的寫法進行了大幅的優化,很多之前的協程寫法不被官方推薦了。如果你之前瞭解過 python 協程,你應該看看最新的用法。

併發、並行、同步和非同步

併發指的是 一個 CPU 同時處理多個程式,但是在同一時間點只會處理其中一個。併發的核心是:程式切換。

但是因為程式切換的速度非常快,1 秒鐘內可以完全很多次程式切換,肉眼無法感知。
bingfa.jpg

並行指的是多個 CPU 同時處理多個程式,同一時間點可以處理多個。
並行.jpg

同步:執行 IO 操作時,必須等待執行完成才得到返回結果。
非同步:執行 IO 操作時,不必等待執行就能得到返回結果。
yibu.jpg

協程,執行緒和程式的區別

多程式通常利用的是多核 CPU 的優勢,同時執行多個計算任務。每個程式有自己獨立的記憶體管理,所以不同程式之間要進行資料通訊比較麻煩。

多執行緒是在一個 cpu 上建立多個子任務,當某一個子任務休息的時候其他任務接著執行。多執行緒的控制是由 python 自己控制的。 子執行緒之間的記憶體是共享的,並不需要額外的資料通訊機制。但是執行緒存在資料同步問題,所以要有鎖機制。

協程的實現是在一個執行緒內實現的,相當於流水線作業。由於執行緒切換的消耗比較大,所以對於併發程式設計,可以優先使用協程。

。。。
這是對比圖:

協程的基礎使用

這是 python 3.7 裡面的基礎協程用法,現在這種用法已經基本穩定,不太建議使用之前的語法了。

import asyncio
import time

async def visit_url(url, response_time):
    """訪問 url"""
    await asyncio.sleep(response_time)
    return f"訪問{url}, 已得到返回結果"

start_time = time.perf_counter()
task = visit_url('http://wangzhen.com', 2)
asyncio.run(task)
print(f"消耗時間:{time.perf_counter() - start_time}")
  • 1, 在普通的函式前面加 async 關鍵字;
  • 2,await 表示在這個地方等待子函式執行完成,再往下執行。(在併發操作中,把程式控制權教給主程式,讓他分配其他協程執行。) await 只能在帶有 async 關鍵字的函式中執行。
  • 3, asynico.run() 執行程式
  • 4, 這個程式消耗時間 2s 左右。

增加協程

再新增一個任務:

task2 = visit_url('http://another.com', 3)
asynicio.run(task2)

這 2 個程式一共消耗 5s 左右的時間。並沒有發揮併發程式設計的優勢。如果是併發程式設計,這個程式只需要消耗 3s,也就是task2的等待時間。要想使用併發程式設計形式,需要把上面的程式碼改一下。

import asyncio
import time

async def visit_url(url, response_time):
    """訪問 url"""
    await asyncio.sleep(response_time)
    return f"訪問{url}, 已得到返回結果"

async def run_task():
    """收集子任務"""
    task = visit_url('http://wangzhen.com', 2)
    task_2 = visit_url('http://another', 3)
    await asyncio.run(task)
    await asyncio.run(task_2)

asyncio.run(run_task())
print(f"消耗時間:{time.perf_counter() - start_time}")

asyncio.gather 會建立 2 個子任務,當出現 await 的時候,程式會在這 2 個子任務之間進行排程。

create_task

建立子任務除了可以用 gather 方法之外,還可以使用 asyncio.create_task 進行建立。

async def run_task():
    coro = visit_url('http://wangzhen.com', 2)
    coro_2 = visit_url('http://another.com', 3)

    task1 = asyncio.create_task(coro)
    task2 = asyncio.create_task(coro_2)

    await task1
    await task2

協程的主要使用場景

協程的主要應用場景是 IO 密集型任務,總結幾個常見的使用場景:

  • 網路請求,比如爬蟲,大量使用 aiohttp
  • 檔案讀取, aiofile
  • web 框架, aiohttp, fastapi
  • 資料庫查詢, asyncpg, databases

進一步學習方向(接下來的文章)

  • 什麼時候用協程,什麼時候用多執行緒,什麼時候用多程式
  • future 物件
  • asyncio 的底層 api
  • loop
  • trio 第三方庫用法

參考文獻

相關文章