1.協程的概念:
協程是一種使用者态的輕量級線程。協程擁有自己的寄存器上下文和棧。
協程排程切換時,将寄存器上下文和棧儲存到其他地方,在切換回來的時候,恢複先前儲存的寄存器上下文和棧。
是以,協程能保留上一次調用時的狀态(即所有局部狀态的一個特定組合),每當程式切換回來時,就進入上一次離開時程式所處的代碼段。
綜合起來,協程的定義就是:
- 必須在隻有一個單線程裡實作并發
- 修改共享資料不需加鎖
- 使用者程式裡儲存多個控制流的上下文棧
- 一個協程遇到IO操作自動切換到其它協程
2.yield實作的協程
傳統的生産者-消費者模型是一個線程生成消息,一個線程取得消息,能過鎖機制控制隊列和等待,但一不小心就有可能死鎖。
如果改用協程,生産者生産消息後,直接通過yield跳轉到消費者開始執行,待消費者執行完畢後,切換加生産者繼續生産,效率較高。
代碼如下:
import time
def consumer():
"""
使用yield生成一個generator生成器
:return:
"""
r = " "
while True:
# yield接收到變量r,處理之後再把結果傳回。函數執行到這一步的時候,函數會停留在這一行上,
#當别的函數執行next()語句或者generator.send()語句來激活這一句,本函數就會
#從yield代碼的下一行開始繼續執行,直到下一次程式循環到yield這裡。
n = yield r
print("[consumer]<-- %s" % n)
time.sleep(1)
r = "ok"
def producer(c):
next(c) #啟動調用consumer()函數中的生成器
n = 0
while n < 10:
n += 1
print("[producer]-->%s" % n)
#生産者生産産品,通過c.send()把程式切換到consumer函數執行
cr = c.send(n)
print("[producer] consumer return:%s" % cr)
c.close()
if __name__ == "__main__":
c1 = consumer()
producer(c1)
執行結果:
[producer]--> 1
[consumer]<-- 1
[producer] consumer return:ok
[producer]--> 2
[consumer]<-- 2
[producer] consumer return:ok
[producer]--> 3
[consumer]<-- 3
[producer] consumer return:ok
... #中間省略
[producer]--> 9
[consumer]<-- 9
[producer] consumer return:ok
[producer]--> 10
[consumer]<-- 10
[producer] consumer return:ok
整個流程是由一個線程執行,producer和consumer協作完成任務,是以稱為協程,而不是線程中的搶占式多任務。
基于協程的定義,剛才使用yield實作的協程并不算合格的協程。
3.由greenlet子產品實作的協程
greenlet機制的主要思想是:生成器函數或者協程函數中的yield語句挂起函數的執行,直到稍後使用next()或send()操作進行恢複主止。可以使用一個排程器循環在一組生成器函數之間協作多個任務。greenlet是python中實作協程的一個子產品。
使用方式 :
from greenlet import greenlet
import time
def func1():
print("func1,ok1---->",time.ctime())
gr2.switch() #程式會切換到func2執行
time.sleep(5) #休眠5s
print("func1,ok2---->",time.ctime())
gr2.switch() #程式又會切換到func2執行
def func2():
print("func2,ok1---->",time.ctime())
gr1.switch() #func2執行到這裡會切換回func1執行
time.sleep(3) #休眠3s
print("func2,ok2---->",time.ctime())
gr1=greenlet(func1)
gr2=greenlet(func2)
gr1.switch()
程式執行流程:
1.程式先運作func1,列印第一句話。
2.func1運作到gr2.switch()這裡時,會切換到func2執行,func2函數列印第一句話。
3.func2執行到gr1.switch()這裡時,又切換回func1函數的time.sleep(5)執行,func1函數會休眠5s。
4.func1先列印第二句話,執行到gr2.switch()這一句時,再次切換回func2函數。
5.func2函數休眠3s,列印func2函數的第二句話,程式執行完畢。
程式執行結果:
func1,ok1----> Fri Jul 21 16:27:11 2017
func2,ok1----> Fri Jul 21 16:27:11 2017
func1,ok2----> Fri Jul 21 16:27:16 2017
func2,ok2----> Fri Jul 21 16:27:19 2017
4.基于greenlet架構,gevent子產品實作協程
python通過yield提供了對協程的基本支援,但是不完全。第三方的gevent子產品提供了協程支援。
gevent是第三方庫,通過greenlet實作協程。
當一個greenlet遇到IO操作時,比如通路網絡,就自動切換到其他的greenlet,等到IO操作完成,再在适當的時候切換回來繼續執行。由于IO操作非常耗時,經常使程式處于等待狀态,有了gevent自動切換協程,就保證總有greenlet在運作,而不是等待IO。
import gevent,time
def func1():
print("running in func1--",time.ctime())
time.sleep(2)
print("running in func1 again--",time.ctime())
def func2():
print("running in func2--",time.ctime())
time.sleep(2)
print("running in func2 again--",time.ctime())
t1=time.time()
g1=gevent.spawn(func1)
g2=gevent.spawn(func2)
gevent.joinall([g1,g2])
t2=time.time()
print("cost time:",t2-t1)
running in func1-- Fri Jul 21 17:20:17 2017
running in func1 again-- Fri Jul 21 17:20:19 2017
running in func2-- Fri Jul 21 17:20:19 2017
running in func2 again-- Fri Jul 21 17:20:21 2017
cost time: 4.007229328155518
可以看到程式是按順序執行的。修改程式,使用gevent.sleep()使程式按協程方式執行。
修改後的代碼如下:
import gevent,time
def func1():
print("running in func1--",time.ctime())
gevent.sleep(2)
print("running in func1 again--",time.ctime())
def func2():
print("running in func2--",time.ctime())
gevent.sleep(2)
print("running in func2 again--",time.ctime())
t1=time.time()
g1=gevent.spawn(func1)
g2=gevent.spawn(func2)
gevent.joinall([g1,g2])
t2=time.time()
print("cost time:",t2-t1)
running in func1-- Fri Jul 21 17:17:00 2017
running in func2-- Fri Jul 21 17:17:00 2017
running in func1 again-- Fri Jul 21 17:17:02 2017
running in func2 again-- Fri Jul 21 17:17:02 2017
cost time: 2.0051145553588867
這樣,程式會先執行func1接着執行的是func2,再切換回func1執行。
這種方式可以使原本需要4s才能執行完成的程式隻需要執行2s就可以了。
gevent.spawn()方法spawn一些任務,然後通過gevent.joinall将任務加入協程執行隊列中等待執行。
5.協程的優點:
無需線程上下文切換造成的資源的浪費。
無需原子操作鎖定及同步的開銷。
友善切換控制流,簡化程式設計模型。
高并發及高擴充性加低成本:一個CPU支援上萬的協程都可以,于高并發處理。
6.協程的缺點:
無法利用多核資源,協程的本質是單個線程,不能同時使用多核CPU。
協程需要與程序配合才能運作在多CPU上。
程式一旦阻塞,會阻塞整個代碼段。