協程,又稱微執行緒,纖程,英文名 Coroutine,
協程是 Python 中另外一種實作多任務的方式,只不過比執行緒更小,占用更小執行單元(理解為需要的資源),
為啥說它是一個執行單元,因為它自帶 CPU 背景關系,這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程, 只要這個程序中保存或恢復 CPU背景關系那么程式還是可以運行的,
通俗的理解:在一個執行緒中的某個函式,可以在任何地方保存當前函式的一些臨時變數等資訊,然后切換到另外一個函式中執行,注意不是通過呼叫函式的方式做到的,并且切換的次數以及什么時候再切換到原來的函式都由開發者自己確定,
協程和執行緒差異
在實作多任務時, 執行緒切換從系統層面遠不止保存和恢復 CPU背景關系這么簡單,
作業系統為了程式運行的高效性每個執行緒都有自己快取 Cache 等等資料,作業系統還會幫你做這些資料的恢復操作,所以執行緒的切換非常耗性能,
但是協程的切換只是單純的操作 CPU 的背景關系,所以一秒鐘切換個上百萬次系統都抗得住,
之前我們講過 yield 關鍵字,現在就用它來實作多任務,
例子:
import time
def task_1():
while True:
print("--1--")
time.sleep(0.5)
yield
def task_2():
while True:
print("--2--")
time.sleep(0.5)
yield
def main():
t1 = task_1()
t2 = task_2()
while True:
next(t1)
next(t2)
if __name__ == "__main__":
main()
運行程序:
先讓 t1 運行一會,當 t1 遇到 yield 的時候,再回傳到 main() 回圈的地方,然后執行 t2 , 當它遇到 yield 的時候,再次切換到 t1 中,這樣 t1 和 t2 就交替運行,最終實作了多任務,協程,
運行結果:

greenlet
為了更好使用協程來完成多任務,Python 中的 greenlet 模塊對其封裝,從而使得切換任務變的更加簡單,
首先你要安裝一下 greenlet 模塊,
pip3 install greenlet
from greenlet import greenlet
import time
def test1():
while True:
print("---A--")
gr2.switch()
time.sleep(0.5)
def test2():
while True:
print("---B--")
gr1.switch()
time.sleep(0.5)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
# 切換到gr1中運行
gr1.switch()
運行結果:

和我們之前用 yield 實作的效果基本一樣,greenlet 其實是對 yield 進行了簡單的封裝,
greenlet 實作多任務要比 yield 更簡單,但是我們以后還是不用它,
上面例子中的延時是0.5秒,如果延遲是100秒,那么程式就會卡住100秒,就算有其他需要執行的任務,系統也不會切換過去,這100秒的時間是無法利用的,
這個問題下面來解決,
gevent
greenlet 已經實作了協程,但是還是得進行人工切換,是不是覺得太麻煩了,
Python 還有一個比 greenlet 更強大的并且能夠自動切換任務的模塊 gevent,
gevent 是對 greenlet 的再次封裝,
其原理是當一個 greenlet 遇到 IO(指的是input output 輸入輸出,比如網路、檔案操作等)操作時,比如訪問網路,就自動切換到其他的 greenlet,等到 IO 操作完成,再在適當的時候切換回來繼續執行,
由于 IO 操作非常耗時,經常使程式處于等待狀態,有了gevent 為我們自動切換協程,就保證總有 greenlet 在運行,而不是等待 IO,
首先還是得先安裝 gevent,
pip3 install gevent
例子:
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()
運行結果:
<Greenlet at 0x35aae40: f(3)> 0
<Greenlet at 0x35aae40: f(3)> 1
<Greenlet at 0x35aae40: f(3)> 2
<Greenlet at 0x374a780: f(3)> 0
<Greenlet at 0x374a780: f(3)> 1
<Greenlet at 0x374a780: f(3)> 2
<Greenlet at 0x374a810: f(3)> 0
<Greenlet at 0x374a810: f(3)> 1
<Greenlet at 0x374a810: f(3)> 2
可以看到,3個 greenlet 是依次運行而不是交替運行,
這還無法判斷 gevent 是否實作了多任務的效果,最好的判斷情況是在運行結果中 0 1 2 不按順序出現,
在 gevent 的概念中,我們提到 gevent 在遇到延時的時候會自動切換任務,
那么,我們先給上面的例子添加延時,再看效果,
import gevent
import time
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
time.sleep(0.5)
g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()
運行結果:
<Greenlet at 0x36aae40: f(3)> 0
<Greenlet at 0x36aae40: f(3)> 1
<Greenlet at 0x36aae40: f(3)> 2
<Greenlet at 0x384a780: f(3)> 0
<Greenlet at 0x384a780: f(3)> 1
<Greenlet at 0x384a780: f(3)> 2
<Greenlet at 0x384a810: f(3)> 0
<Greenlet at 0x384a810: f(3)> 1
<Greenlet at 0x384a810: f(3)> 2
在添加了延時之后,運行結果并沒有改變,
其實,gevent 要的不是 time.sleep() 的延時,而是 gevent.sleep() 的延時,
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(0.5)
g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()
join 還有一種更簡單的寫法,
import time
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(0.5)
gevent.joinall([
gevent.spawn(f, 3),
gevent.spawn(f, 3),
gevent.spawn(f, 3)
])
一般都是后面的這種寫法,
運行結果:
<Greenlet at 0x2e5ae40: f(3)> 0
<Greenlet at 0x2ffa780: f(3)> 0
<Greenlet at 0x2ffa810: f(3)> 0
<Greenlet at 0x2e5ae40: f(3)> 1
<Greenlet at 0x2ffa780: f(3)> 1
<Greenlet at 0x2ffa810: f(3)> 1
<Greenlet at 0x2e5ae40: f(3)> 2
<Greenlet at 0x2ffa780: f(3)> 2
<Greenlet at 0x2ffa810: f(3)> 2
這下終于實作多任務的效果了, gevent 在遇到延時的時候,就自動切換到其他任務,
這里是將 time 中的 sleep 換成了 gevent 中的 sleep,
那如果有網路程式,網路程式中也有許多堵塞,比如 connect, recv,accept,需要不需要換成 gevent 中的對應方法,
理論上來說,是要換的,如果想用 gevent,那么就要把所有的延時操作,堵塞這一類的函式,統統換成 gevent 中的對應方法,
那有個問題,萬一我的代碼已經寫了10萬行了,這換起來怎么破......
有什么辦法不需要手動修改么,有,打個補丁即可,
import time
import gevent
from gevent import monkey
# 有耗時操作時需要
# 將程式中用到的耗時操作的代碼,換為gevent中自己實作的模塊
monkey.patch_all()
def f(n):
for i in range(n):
print(gevent.getcurrent(), i)
time.sleep(0.5)
g1 = gevent.spawn(f, 3)
g2 = gevent.spawn(f, 3)
g3 = gevent.spawn(f, 3)
g1.join()
g2.join()
g3.join()
monkey.patch_all() 會自動去檢查代碼,將所有會產生延時堵塞的方法,都自動換成 gevent 中的方法,
運行結果:
<Greenlet at 0x3dd91e0: f(3)> 0
<Greenlet at 0x3dd9810: f(3)> 0
<Greenlet at 0x3dd99c0: f(3)> 0
<Greenlet at 0x3dd91e0: f(3)> 1
<Greenlet at 0x3dd9810: f(3)> 1
<Greenlet at 0x3dd99c0: f(3)> 1
<Greenlet at 0x3dd91e0: f(3)> 2
<Greenlet at 0x3dd9810: f(3)> 2
<Greenlet at 0x3dd99c0: f(3)> 2
總結:
通過利用延時的時間去做其他任務,把時間都利用起來,這就是協程最大的意義,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/179757.html
標籤:Python
上一篇:Java NIO
