天天看點

python中的協程及實作

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上。
程式一旦阻塞,會阻塞整個代碼段。