查看:https://blog.csdn.net/qq_27825451/article/details/85234610
一,什么是協程(coroutine)
1,協程定義
協程,又稱微線程,纖程。英文名Coroutine。協程的概念很早就提出來了,但直到最近幾年才在某些語言(如Lua)中得到廣泛應用。
2,子程序,或者稱為函數
在所有語言中都是層級調用,比如A調用B,B在執行的過程中又調用了C,C執行完畢返回,B執行完畢返回,最后是A執行完畢。所以子程序調用是通過棧實現的,一個線程就是執行一個子程序。子程序調用總是一個入口,一個返回,調用的順序是明確的。
順序執行的缺點:這里小伙伴們都應該是分清楚,那就是程序無休此等待,必須等待一個函數執行完之后才返回結果。
3,多線程
避免順序執行的方式之一是多線程,但是考慮到python語言的特性(GIL鎖),再執行計算密集型的任務時,多線程的執行效果反而變慢,再執行IO密集型的任務時候雖然有不錯的性能提升,但是依然會有線程管理與切換、同步的開銷等等(具體原因這里不詳細說明,請參見相關的GIL說明)
4,協程
協程有點像多線程,但協程的特點在於是一個線程執行,那和多線程比,協程有何優勢?
優勢一:最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是又程序自身控制,因此沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
優勢二:就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多。
5,多進程+協程
因為協程是一個線程執行,那怎么利用多核CPU呢?最簡單的方法是多進程+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的性能。
6,協程於一般函數的不同點
協程看上去也是子程序(函數),但執行過程中,在子程序內部(函數)可中斷,而不是一次性一定要執行完才行,然后轉而執行別的子程序,在適當的時候再返回來接着執行。
二,協程(coroutine的直觀理解)
1,協程的直觀理解
yield個人認為其實是為了實現協程而出現的。所以如果要解釋清楚什么是yield,那么也就必須要先搞懂什么是協程。首先明確一點:協程是針對單個CPU的,也就是說,講協程講的就是單線程。我們可以通過協程實現類似並發的任務,並且如果只是在一個CPU上的話,使用協程帶來的效率一般都會比使用線程來的高。這是為啥呢?這就要看協程的原理了。
協程的原理很簡單,打個比方就能講明白了:假設說有十個人去食堂打飯,這個食堂比較窮,只有一個打飯的窗口,並且也只有一個打飯阿姨,那么打飯就只能一個一個排隊來打咯。這十個人胃口很大,每個人都要點5個菜,但這十個人又有個毛病就是做事情都猶豫不決,所以點菜的時候就會站在那里,每點一個菜后都會想下一個菜點啥,因此后面的人等的很着急呀。這樣一直站着也不是個事情吧,所以打菜的阿姨看到某個人猶豫5秒后就開始吼一聲,會讓他排到隊伍最后去,先讓別人打菜,等輪到他的時候他也差不多想好吃啥了。這確實是個不錯的方法,但也有一個缺點,那就是打菜的阿姨會等每個人5秒鍾,如果那個人在5秒內沒有做出決定吃啥,其實這5秒就是浪費了。一個人點一個菜就是浪費5秒,十個人每個人點5個菜可就浪費的多啦(菜都涼了要)。那咋辦呢?
這個時候阿姨發話了:大家都是學生,學生就要自覺,我以后也不主動讓你們排到最后去了,如果你們覺得自己會猶豫不決,就自己主動點直接點一個菜就站后面去,等下次排到的時候也差不多想好吃啥了。這個方法果然有效,大家點了菜后想的第一件事情不是下一個菜吃啥,而是自己會不會猶豫,如果會猶豫那直接排到隊伍后面去,如果不會的話就直接接着點菜就行了。這樣一來整個隊伍沒有任何時間是浪費的,效率自然就高了。
這個例子里的排隊阿姨的那聲吼就是我們的CPU中斷,用於切換上下文。每個打飯的學生就是一個task。而每個人自己決定自己要不要讓出窗口的這種行為,其實就是我們協程的核心思想。
在用線程的時候,其實雖然CPU把時間給了你,你也不一定有活干,比如你要等IO、等信號啥的,這些時間CPU給了你你也沒用呀。
在用協程的時候,CPU就不來分配時間了,時間由你們自己決定,你覺得干這件事情很耗時,要等IO啥的,你就干一會歇一會,等到等IO的時候就主動讓出CPU,讓別人上去干活,別人也是講道理的,干一會也會把時間讓給你。協程就是使用了這種思想,讓編程者控制各個任務的運行順序,從而最大可能的發揮CPU的性能。
2,為什么yield可以實現協程
在Python中,協程通過yield實現。因為當一個函數中又yield存在的時候,這個函數是生成器,那么當你調用這個函數的時候,你在函數體重寫的代碼並沒有被執行,而是返回了一個生成器對象,這個需要特別注意。然后,你的代碼會在每次使用這個生成器的時候被執行。
前面講過yiedl表達式的兩個關鍵作用:①返回一個值②接收調用者參數
“調用者”與“被調用者”之間的通信是通過send()進行聯系的
正是因為yield實現的生成器具備“中斷等待的功能”,才使得yield可以實現協程。
3,yield實現協程的例子
(1)實例一:
生產者-消費者模型。這里不討論生產者-消費者模式到底有什么用,這里要實現的就是簡單的函數調用。代碼如下:
生產者消費者.py
def consumer(): # 定義返回 r = '' while True: # 執行的中斷點 n = yield r # 如果n為空則return生成器拋出StopIteration異常 # 實際本例n一直不為空這一段永不執行 if not n: return # 消費者消費通過生產者send(value)過來的參數 print('[消費者]正在消費:{0}'.format(n)) # 設置返回值r r = '200 人民幣' def producer(c): # 啟動消費者(生成器),實際上是函數調用,只不過生成器不是直接像函數那般調用 # 使用send()第一次啟動生成器傳遞的參數只能是None否則報錯 c.send(None) n = 0 while n < 5: n = n + 1 print('[生產者]正在生產:{0}'.format(n)) # 生產者往消費者發送消息 # 給消費者傳入值,實際也是函數調用 r = c.send(n) print('[生產者]消費者返回:{0}'.format(r)) print('-------------------------------') # 關閉生成器 c.close() c = consumer() producer(c)
運行輸出如下
PS D:\learn-python3\函數式編程> & c:/python36/python.exe d:/learn-python3/學習腳本/協程系列/生產者消費者.py [生產者]正在生產:1 [消費者]正在消費:1 [生產者]消費者返回:200 人民幣 ------------------------------- [生產者]正在生產:2 [消費者]正在消費:2 [生產者]消費者返回:200 人民幣 ------------------------------- [生產者]正在生產:3 [消費者]正在消費:3 [生產者]消費者返回:200 人民幣 ------------------------------- [生產者]正在生產:4 [消費者]正在消費:4 [生產者]消費者返回:200 人民幣 ------------------------------- [生產者]正在生產:5 [消費者]正在消費:5 [生產者]消費者返回:200 人民幣
解釋分析:
第一步:在produece(c)函數中,調用c.send(None)啟動了生成器,這相當於調用consumer(),但是如果consumer是一個普通函數而不是生成器,就要等到consumer執行完了,主動權才會回到produce手里。但就是因為consumer是生成器,所以第一次遇到yield暫停。接着執行produce()中接下來的代碼,從運行結果看,確實打印出了[生產者]正在生產1,當程序運行至c.send(n)時,再次調用生產並且通過yiele傳遞了參數(n=1),這個時候進入consumer()函數先前在yield停下的地方,繼續往后執行,所以打印出[消費者]正在消費1.
第二步:[消費者]正在消費1這句話被打印出來之后,接下來consumer()函數中此時r被賦值為'200 人民幣',接着cousumer()函數里面的第一次循環結束,進入第二次循環,又遇到yield,所以consumer()函數又暫停並且返回變量r的值,consumer()函數暫停,此時程序有進入produce()函數中接着執行。
第三步:由於先前produce(c)函數接着第一次循環中c.send(n)處相當於調用消費者consumer(),跳入到了consumer()里面去執行,現在cousumer()暫停,prodecer重新掌握主動權,故而繼續往下執行打印出[生產者]消費者返回:200 人民幣,然后打印出分隔符之后,prodecer的第一次循環結束,並進行第二次信息,打印出[生產者]正在生產2,然后又調用c.send(n)又調用消費者consumer,將控制器交給consumer,如此循環...
(2)實例二
import time #定義一個消費者,他有名字name #因為里面有yield,本質上是一個生成器 def consumer(name): print(f'{name} 准備吃包子啦!,呼吁店小二') while True: baozi=yield #接收send傳的值,並將值賦值給變量baozi print(f'包子 {baozi+1} 來了,被 {name} 吃了!') #定義一個生產者,生產包子的店家,店家有一個名字name,並且有兩個顧客c1 c2 def producer(name,c1,c2): next(c1) #啟動生成器c1 next(c2) #啟動生成器c2 print(f'{name} 開始准備做包子啦!') for i in range(5): time.sleep(1) print(f'做了第{i+1}包子,分成兩半,你們一人一半') c1.send(i) c2.send(i) print('------------------------------------') c1=consumer('張三') #把函數變成一個生成器 c2=consumer('李四') producer('店小二',c1,c2)
運行結果
D:\learn-python3\函數式編程> & c:/python36/python.exe d:/learn-python3/學習腳本/協程系列/生產包子.py 張三 准備吃包子啦!,呼吁店小二 李四 准備吃包子啦!,呼吁店小二 店小二 開始准備做包子啦! 做了第1包子,分成兩半,你們一人一半 包子 1 來了,被 張三 吃了! 包子 1 來了,被 李四 吃了! ------------------------------------ 做了第2包子,分成兩半,你們一人一半 包子 2 來了,被 張三 吃了! 包子 2 來了,被 李四 吃了! ------------------------------------ 做了第3包子,分成兩半,你們一人一半 包子 3 來了,被 張三 吃了! 包子 3 來了,被 李四 吃了! ------------------------------------ 做了第4包子,分成兩半,你們一人一半 包子 4 來了,被 張三 吃了! 包子 4 來了,被 李四 吃了! ------------------------------------ 做了第5包子,分成兩半,你們一人一半 包子 5 來了,被 張三 吃了! 包子 5 來了,被 李四 吃了! ------------------------------------
運行過程分析:
第一步:啟動生成器c1,c2.c1先運行,運行到第一個循環的yield,暫停,然后c2運行,也運行到第一個yield暫停,打印得到
張三 准備吃包子啦!,呼吁店小二
李四 准備吃包子啦!,呼吁店小二
第二步:現在相當於兩個顧客等着吃包子,控制權交給店小二生產包子,於是打印出 店小二 開始准備做包子啦!,並且進入producer的第一個循環,花了1秒鍾,生產第一個包子,然后將其一分為二,打印出:做了第1包子,分成兩半,你們一人一半。
第三步:此時producer店小二調用send()函數,相當於將包子給兩位客人,這個時候先執行c1.send(),即先把包子給c1,然后c1獲得了控制權,打印出包子 1 來了,被 張三 吃了!然后他吃完進入第二次循環遇見了yield,又暫停。控制權重新回到producer手上,他再執行c2.send(),將包子給c2,c2掌握控制權,於是打印出 包子 1 來了,被 李四 吃了!它在進入第二次循環,遇到yield,然后又暫停了,控制權重新回到producer店小二手中,店小二打印出一段虛線,然后進入第二次循環,重新花了1秒鍾,又做了一個包子,一次這樣下去。
(3)實例三
往生成器傳遞數字計算平均值
def average(): # 數字的總和 total = 0.0 # 數字的個數 count = 0 # 數字的平均值 avg = None while True: # 平均值作為生成器的返回 n = yield avg # 沒跌倒一次生成器數字傳遞的數字個數+1 count = count + 1 # 計算傳遞數字的和 total = total + n # 計算平均值 avg = total / count a = average() a.send(None) # 定義一個函數,通過這個函數想average函數發送數值 def sender(generator): # 第一步啟動生成器 next(generator) print(generator.send(10)) print(generator.send(20)) print(generator.send(30)) print(generator.send(40)) g = average() sender(g) # 10.0 # 15.0 # 20.0 # 25.0
運行步驟為
第一步:啟動生成器,停留在yield
第二步:使用send(10)傳遞數字10進入生成器計算平均值為10,作為生成器yield的返回值,然后依次傳遞20,30,40,依次打印計算出來的平均值
三,協程的狀態查看
我們知道,協程是可以暫停等待的,然后又恢復的生成器函數,那么我們有沒有什么辦法查看一個協程到低是處於什么狀態呢?協程有四種狀態,它們分別是:
GEN_CREATED:等待執行,即還沒有進入協程
GEN_RUNNING:解釋器執行(這個只有在使用多線程時才能查看到他的狀態,而協程是單線程的)
GEN_SUSPENDED:在yield表達式處暫停(協程在暫停等待的時候的狀態)
GEN_CLOSED:執行結束(協程執行結束了之后的狀態)
怎么查看呢?
協程的狀態可以用inspect.getgeneratorstate()函數來確定,實例如下:
協程的狀態.py
from inspect import getgeneratorstate #一定要導入 from time import sleep def my_generator(): for i in range(3): sleep(0.5) x = yield i + 1 g=my_generator() #創建一個生成器對象 def main(generator): try: print("生成器初始狀態為:{0}".format(getgeneratorstate(g))) next(g) #激活生成器 print("生成器初始狀態為:{0}".format(getgeneratorstate(g))) g.send(100) print("生成器初始狀態為:{0}".format(getgeneratorstate(g))) next(g) print("生成器初始狀態為:{0}".format(getgeneratorstate(g))) next(g) except StopIteration: print('全部迭代完畢了') print("生成器初始狀態為:{0}".format(getgeneratorstate(g)))
運行結果如下
PS D:\learn-python3\函數式編程> & c:/python36/python.exe d:/learn-python3/學習腳本/協程系列/協程的狀態.py 生成器初始狀態為:GEN_CREATED 生成器初始狀態為:GEN_SUSPENDED 生成器初始狀態為:GEN_SUSPENDED 生成器初始狀態為:GEN_SUSPENDED 全部迭代完畢了 生成器初始狀態為:GEN_CLOSED
可以看到創建一個生成器沒有初始化處於等待狀態,一旦開始跌打就處於yield暫停等待狀態,迭代完畢或者手動關閉處於關閉狀態。
四,yield實現協程的不足之處
(1)協程函數的返回值不是特別方便獲取,為什么參見上一篇文章,只能夠通過捕獲StopIteration異常,然后通過該異常的value屬性獲取。
(2)Python的生成器是協程corontine的一種形式,但它的局限性在於只能向它的直接調用者每次yield一個值。這意味着那些包含yield的代碼不能像其他代碼那樣被分離出來放到一個單獨的函數中。這也正是yield from要解決的。
全文總結
從某些角度來理解,協程其實就是一個可以暫停執行的函數,並且可以恢復繼續執行。那么yield已經可以暫停執行了,如果在暫停后有辦法把一些 value 發回到暫停執行的函數中,那么 Python 就有了『協程』。於是在PEP 342中,添加了 “把東西發送到已經暫停的生成器中” 的方法,這個方法就是send()。
下文預告:下一篇講解yield from的詳細使用方法