這篇文章給大家分享的內容是深入了解python中的協程函數,有著一定的參考價值,有需要的朋友可以參考一下
根據維基百科給出的定義,「協程是為非搶佔式多任務產生子程式的電腦程式元件,協程允許不同入口點在不同位置暫停或開始執行程式」。從技術的角度來說,「協程就是你可以暫停執行的函數」。如果你把它理解成“就像生成器一樣”,那麼你就想對了。
協程,又稱為微線程,看上去像是子程序,但是它和子程序又不太一樣,它在執行的過程中,可以在中斷當前的子程序後去執行別的子程序,再返回來執行之前的子程序,但是它的相關資訊還是之前的。
協程不同於執行緒,執行緒是搶佔式的調度,而協程是協同式的調度,協程則需要自己做調度。
子程式呼叫總是一個入口,一次返回,呼叫順序是明確的。而協程的呼叫和子程序不同。協程看起來也是子程序,但執行過程中,在子程序內部可中斷,然後轉而執行別的子程序,在適當的時候再返回來接著執行。
協程的優點:
協程優勢是極高的執行效率。因為子程式切換不是執行緒切換,而是由程式本身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的效能優勢就越明顯。用來執行協程多任務非常適合。
協程沒有執行緒的安全性問題。一個行程可以同時存在多個協程,但是只有一個協程是啟動的,而且協程的啟動和休眠又是程式設計師透過程式設計來控制,而不是作業系統控制的。
#範例:
def func(n): index=0 if index<=n: c=yield 1 print("task------{}".format(c)) index+=1f=func(3) n=next(f) print(n)try: n=f.send(5)#程序就直接结束了 print("n是{}".format(n))except StopIteration as e: pass
输出打印:1task------5
解釋說明:
很明顯func是一個產生器,send方法有一個參數,該參數指定的是上一次被掛起的yield語句的回傳值。
send需要做例外處理。
總的來說,send方法和next方法唯一的差別在於執行send方法會先把上一次掛起的yield語句的回傳值透過參數設定,從而實現與生成器方法的交互作用。但要注意,在一個生成器物件沒有執行next方法之前,由於沒有yield語句被掛起,所以執行send方法會報錯。
send方法的參數為None時,它與next方法完全等價。
生成器實作生產者與消費者模式:
def cunsumer(): while True: n=yield 3 if not n: return print('cunsumer{}'.format(n))def product(c): c.send(None) n=0 while n<5: n=n+1 r=c.send(n) print("product{}".format(r)) c.close() c=cunsumer() product(c)
打印: cunsumer1 product3 cunsumer2 product3 cunsumer3 product3 cunsumer4 product3 cunsumer5 product3
說明:
在生產者先執行了c.send(None),目的是先讓消費者掛起,再用send傳值,第一次傳1,消費者那裡打印1,生產者打印r是消費者yield後面的值。
雖然CPython(標準Python)能夠透過生成器來實現協程,但使用起來還並不是很方便。
同時,Python的一個衍生版 Stackless Python實作了原生的協程,它更有利於使用。
於是,大家開始將 Stackless 中關於協程的程式碼 單獨拿出來做成了CPython的擴充包。
這就是 greenlet 的由來,因此 greenlet 是底層實作了原生協程的 C擴充函式庫。
程式碼示意:
from greenlet import greenletimport randomimport timedef Producer(): while True: item = random.randint(0,10) print("生产了{}".format(item)) c.switch(item)#切换到消费者,并将item传入消费者 time.sleep(1)def consumer(): print('我先执行') #p.switch() while True: item = p.switch()#切换到生产者,并且等待生产者传入item print('消费了{}'.format(item)) c = greenlet(consumer)#将一个普通函数变成一个协程p = greenlet(Producer) c.switch()#让消费者先进入暂停状态(只有恢复了才能接收数据)
greenlet 的價值:
高效能的原生協程
語意更明確的明確切換
#直接將函數包裝成協程,保持原始程式碼風格
雖然,我們有了基於epoll 的回呼程式設計模式,但卻很難使用。
即使我們可以透過配合 生成器協程 進行複雜的封裝,以簡化程式設計難度。
但仍然有一個大的問題: 封裝難度大,現有程式碼幾乎完全要重寫
gevent,透過封裝了 libev(基於epoll) 和 greenlet 兩個函式庫。
幫我們做好封裝,讓我們以類似執行緒的方式使用協程。
以至於我們幾乎不用重寫原來的程式碼就能充分利用 epoll 和 協程 威力。
程式碼示意:
from gevent import monkey;monkey.patch_all()#会把python标准库当中一些阻塞操作变成非阻塞import geventdef test1(): print("11") gevent.sleep(4)#模拟爬虫请求阻塞 print("33")def test2(): print("22") gevent.sleep(4) print("44") gevent.joinall([gevent.spawn(test1),gevent.spawn(test2)])#joinall 阻塞当前协程,执行给定的greenlet#spawn 启动协程,参数就是函数的名字
gevent 的價值:
遇到阻斷就切換到另一個協程繼續執行!
使用基於 epoll 的 libev 來避免阻斷。
使用基於 gevent 的 高效能協程 來切換執行。
只在遇到阻塞的時候切換,沒有輪需的開銷,也沒有執行緒的開銷。
gevent實作並發伺服器
from gevent import monkey;monkey.patch_all() #建议放在首行,会把python标准库当中一些阻塞操作变成非阻塞import geventimport socket server=socket.socket() server.bind(('',6666)) server.listen(5) print("开始监听")def readable(con,addr): print("客户端{}接入".format(addr)) while True: data=con.recv(1024) if data: print(data) else: con.close() breakwhile True: con,addr=server.accept() gevent.spawn(readable,con,addr)#将readable函数变为协程,并且把con和addr传入其中。
gevent也有自己的佇列。使用方式和進線程基本上一樣。
基於gevent和佇列的生產者和消費者模式
from gevent import monkey;monkey.patch_all()import geventfrom gevent.queue import Queueimport randomdef producter(queue): while True: item=random.randint(0,99) print('生产了{}'.format(item)) queue.put(item) gevent.sleep(1)def comuser(queue): while True: item=queue.get() print('消费了{}'.format(item)) queue=Queue() p=gevent.spawn(producter,queue) c=gevent.spawn(comuser,queue) gevent.joinall([p,c])
打印: 生产了33消费了33生产了95消费了95生产了92消费了92...
相关推荐:
以上是深入了解python中的協程函數的詳細內容。更多資訊請關注PHP中文網其他相關文章!