31. 協程的使用

星光映梦發表於2024-11-12

一、什麼是協程

  從 Python 3.4 開始,Python 加入了協程的概念,使用 asyncio 模組實現協程。但這個版本的協程還是以生成器物件為基礎。 Python 3.5 中增加了 async、await 關鍵字,使協程的實現更加方便。

  協程(Coroutine),又稱 微執行緒,是一種執行執行在使用者態的輕量級執行緒。協程可以在單執行緒的情況下實現併發。

  協程擁有自己的暫存器上下文和棧。協程在排程切換時,將暫存器下上文和棧儲存到其它地方,等切換回來的時候,再恢復先前儲存的暫存器上下文和棧。因此,協程能保留上一次呼叫時的狀態,即所有區域性狀態的一個特定組合,每次過程重入,就相當於進入上一次呼叫的狀態。

  協程本質上是個單程序,相對於多程序來說,它沒有執行緒上下文切換的開銷,沒有原子操作鎖定及同步的開銷。

  關於協程,我們還需要了解以下幾個概念:

  • event_loop:事件迴圈,相當於無限迴圈,我們可以把一些函式註冊到這個事件迴圈上,當滿足發生條件的時候,就呼叫對應的處理方法;
  • coroutine:在 Python 中常指代協程物件型別,我們可以將協程物件註冊到事件迴圈中,它會被事件迴圈呼叫。我們可以使用 async 關鍵字來定義一個方法,這個方法在呼叫不會立即被執行,而是會返回一個協程物件;
  • task:任務,這是協程物件的進一步封裝,包含協程物件的各個狀態;
  • future:代表將來執行或者沒有執行的任務的結果,實際上和 task 沒有本質區別;

二、協程的使用

import asyncio
import time

# 協程函式,定義函式的時候,使用async關鍵字修飾的函式
async def work(x):
    print(f"work({x}) start!")
    print(f"當前任務的引數為:{x}")

    # 遇到IO操作掛起當前協程(任務),等待IO操作完成之後再繼續往下執行
    # 當前協程掛起時,事件迴圈可以去執行其它任務
    await asyncio.sleep(x)

    print(f"work({x}) end!")

    return f"當前任務的返回值:{x}"

async def main():
    print("開始執行main()函式內部程式碼!")

    tasks = [
        # asyncio.create_task()建立一個task物件
        asyncio.create_task(work(2)),
        # asyncio.ensure_future()建立一個task物件
        asyncio.ensure_future(work(3)),
        asyncio.create_task(work(5)),
    ]

    # asyncio.wait()接收的引數為task物件,返回一個二值元組
    # done接收任務的返回值,pending接收任務的狀態
    done, pending = await asyncio.wait(tasks)
    for item in done:
        print(item)

    print("main()函式內部程式碼執行完畢!")

if __name__ == "__main__":
    start  = time.time()

    # 協程物件,執行協程函式()得到的協程物件
    # 執行協程函式建立協程物件,函式內部的程式碼不會執行
    result = main()

    # 去生成或獲取一個事件迴圈
    # loop = asyncio.get_event_loop()
    # 將任務放到任務列表中
    # loop.run_until_complete(result)

    # Python 3.7之後的版本可以簡化為如下程式碼
    asyncio.run(result)

    print(time.time() - start)

  協程函式 就是使用 async 關鍵字修飾的函式。協程物件 就是執行協程函式得到的物件。執行協程函式時,協程函式內部的程式碼不會執行。如果想要執行協程函式內部程式碼,必須要將協程物件交給事件迴圈來處理。當我們把協程物件傳遞給 run_until_complete() 方法時,實際它將 coroutine 封裝成 task 物件。

  事件迴圈 可以理解為一個死迴圈,它迴圈檢測並執行某些程式碼,它的實現原理如下:

任務列表 = [任務1, 任務2, 任務3, ...]

while True:
    可執行的任務列表, 已完成的列表列表 = 去任務列表中檢查所有的任務,將“可執行”和“已完成”的任務返回
    for 就緒任務 in 可執行的任務列表:
        執行已就緒的任務

    for 已完成的任務 in 已完成的任務列表:
        在任務列表中移除“已完成”的任務

    if 任務列表中的任務都已完成:
        break

  要實現非同步處理,先要有掛起操作,當一個任務需要等待 IO 結果的時候,可以掛起當前任務,轉而執行其它任務,這樣才能充分利用好資源。

  await 關鍵字可以將耗時等待的操作掛起,讓出控制權。如果協程在執行的時候遇到 await,事件迴圈就會將本協程掛起,轉而執行別的協程,直到其它協程掛起或執行完畢。

  await 後面的物件必須是如下格式之一:

  • 一個原生的協程物件;
  • 一個由 types.coroutine 修飾的生成器,這個生成器可以返回協程物件;
  • 有一個包含 __await__ 方法的物件返回的一個迭代器;

三、手動實現協程

  程序、執行緒建立完之後,到底是哪個程序、執行緒執行去執行,這是不確定的,這需要有作業系統來進行計算(排程演算法,例如優先順序排程)。而協程是可以人為來控制的。程式設計師在程式碼層面上檢測所有的 IO 操作,一旦遇到 IO 操作,程式設計師在程式碼級別完成切換,這樣給 CPU 的感覺就是這個程式一直執行,沒有 IO 操作,從而提升程式的執行效率。

3.1、使用greenlet模組實現協程

  我們可以使用 greenlet 模組實現協程,但遇到 IO(指的是 input output,輸入輸出,比如,網路、檔案操作等) 操作時,還需要人工切換。

  我們可以在終端中使用 pip 命令安裝 greelet 模組:

pip install greenlet
import time

from greenlet import greenlet

def task1():
    while True:
        print("task1 work!")
        g2.switch()
        time.sleep(0.5)

def task2():
    while True:
        print("task2 work!")
        g1.switch()
        time.sleep(0.5)

if __name__ == "__main__":
    g1 = greenlet(task1)
    g2 = greenlet(task2)

    g1.switch()

3.2、使用gevent模組實現協程

  greenlet 模組已經實現了協程,但還需要人工切換。Python 還提供了一個能自動切換任務的 gevent 模組。其原理是當一個 greenlet 遇到 IO 就會自動切換到其它的 greenlet 再執行,而不是等待 IO。

  我們可以在終端中使用 pip 命令安裝 gevent 模組:

pip install gevent
import time

from gevent import spawn

# gevent模組本身無法檢測常見的一些IO操作
# 在使用的時候需要額外匯入monkey模組
from gevent import monkey
monkey.patch_all()

def say_hello():
    print("hello")
    time.sleep(2)
    print("hello")

def say_hi():
    print("hi")
    time.sleep(3)
    print("hi")

def say_good():
    print("good")
    time.sleep(5)
    print("good")

start_time = time.time()

g1 = spawn(say_hello)
g2 = spawn(say_hi)
g3 = spawn(say_good)

# 等待被檢測的任務執行完畢,再往後執行
g1.join()
g2.join()
g3.join()

print(time.time() - start_time)
import gevent
import time

# gevent模組本身無法檢測常見的一些IO操作
# 在使用的時候需要額外匯入monkey模組
from gevent import monkey
monkey.patch_all()

def say_hello():
    print("hello")
    time.sleep(2)
    print("hello")

def say_hi():
    print("hi")
    time.sleep(3)
    print("hi")

def say_good():
    print("good")
    time.sleep(5)
    print("good")

start_time = time.time()

gevent.joinall([
    gevent.spawn(say_hello),
    gevent.spawn(say_hi),
    gevent.spawn(say_good),
])

print(time.time() - start_time)

time 模組的 sleep() 方法不具備自動切換任務的功能,而 gevent 模組的 sleep() 方法具有該能功能,所以我們使用猴子補丁將 time 模組的 sleep() 方法程式設計 gevent 模組的 sleep() 方法;

相關文章