一、什麼是協程
從 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() 方法;