python中的協程及實現

任平生78發表於2017-07-21

1.協程的概念:

協程是一種使用者態的輕量級執行緒。協程擁有自己的暫存器上下文和棧。

協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切換回來的時候,恢復先前儲存的暫存器上下文和棧。

因此,協程能保留上一次呼叫時的狀態(即所有區域性狀態的一個特定組合),每當程式切換回來時,就進入上一次離開時程式所處的程式碼段。

綜合起來,協程的定義就是:

  1. 必須在只有一個單執行緒裡實現併發
  2. 修改共享資料不需加鎖
  3. 使用者程式裡儲存多個控制流的上下文棧
  4. 一個協程遇到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上。
程式一旦阻塞,會阻塞整個程式碼段。


相關文章