想了解更多精彩內容,快來關注一支穿雲
1、概念
1。1 協程
協程:比執行緒更小的執行單元,又稱微執行緒,在單執行緒上執行多個任務,自帶CPU上下文。用函式切換,開銷極小。不透過作業系統排程,沒有程序、執行緒的切換開銷。
那麼執行緒與協程有什麼區別呢?
我們假設把一個
程序
比作我們實際生活中的一個
拉麵館
,負責保持拉麵館執行的
服務員
就是
執行緒
,每個餐桌點菜代表要完成的任務。
當我們用
多執行緒
完成任務時,模式是這樣的:每來一桌的客人,就在那張桌子上安排一個服務員負責,即有多少桌客人就得對應多少個服務員;
而當我們用
協程
來完成任務時,模式卻有所不同了: 就安排一個服務員,來吃飯得有一個
點餐和等菜的過程
,當A在點菜,就去給B服務,B叫了菜在等待,我就去C,當C也在等菜並且A點菜點完了,趕緊到A來服務… …依次類推。
從上面的例子可以看出,
想要使用協程,那麼我們的任務必須有等待
。
當我們要完成的任務是耗時任務時,比如屬於IO密集型任務時,我們使用協程來執行任務會節省很多的資源(一個服務員和多個服務員的區別),並且可以極大的利用到系統的資源。
協程,是單執行緒下的併發
,又稱微執行緒,英文名Coroutine。是一種使用者態的輕量級執行緒,即協程是由使用者程式自己控制排程的。協程能保留上一次呼叫時的狀態,每次過程重入時,就相當於進入上一次呼叫的狀態,換種說法:進入上一次離開時所處邏輯流的位置,當程式中存在大量不需要CPU的操作時(IO),適用於協程。【在一個執行緒中CPU來回切換執行不同的任務,這種現象就是協程】
協程有極高的執行效率,因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷。
不需要多執行緒的鎖機制,因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。
因為協程是一個執行緒執行,所以想要利用多核CPU,最簡單的方法是多程序+協程,這樣既充分利用多核,又充分發揮協程的高效率。
那符合什麼條件就能稱之為協程:
1、必須在只有一個單執行緒裡實現併發
2、修改共享資料不需加鎖
3、使用者程式裡自己儲存多個控制流的上下文棧
4、一個協程遇到IO操作自動切換到其它協程
python中對於協程有四個模組,
greenlet、gevent、yield和async來實現切換和儲存執行緒
。
1。2 yield實現任務切換+儲存執行緒
執行效果:
yield檢測不到IO,無法實現遇到IO自動切換。
1。3 greenlet是手動切換
執行效果輸出:
開門走進衛生間
一看拖把放旁邊
飛流直下三千尺
疑是銀河落九天
greenlet只是提供了一種比yield(生成器)更加便捷的切換方式,當切到一個任務執行時如果遇到IO,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提升效率的問題。
1。4 Gevent實現自動切換協程(多協程)
協程的本質就是在單執行緒下,由使用者自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,以此來提升效率。
一般在工作中我們都是
程序+執行緒+協程
的方式來
實現併發
,以達到最好的併發效果。
如果是4核的CPU,一般起5個程序,每個程序中20個執行緒(5倍CPU數量),每個執行緒可以起500個協程,大規模爬取頁面的時候,等待網路延遲的時間的時候,我們就可以用協程去實現併發。併發數量=5
20
500從而達到5000個併發,這是一般一個4個CPU的機器最大的併發數。nginx在負載均衡的時候最大承載量是5w個。
單執行緒裡的這20個任務的程式碼通常既有計算操作又有阻塞操作,我們完全可以在執行任務1時遇到阻塞,就利用阻塞的時間去執行任務2。。。如此,才能提高效率,這就用到了Gevent模組。
Gevent(自動切換,由於切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,這一過程在啟動時透過monkey patch完成)。
monkey.patch_all() 一定要放到匯入requests模組之前,否則gevent無法識別requests的阻塞。
1。5 async實現協程
3、關鍵字:yield
3。1 yield表示式
yield
相當於return,只不過return是終結函式並返回一個值,而yield是先把值返回並把函式掛起來,以後還會執行yield以下的語句。
輸出:
1、第一次ti呼叫next函式時,進入foo函式,遇到yield就把count=0返回,並把foo函式掛起
2、在for迴圈中再次呼叫next函式時,就開始執行yield後面的賦值語句,由於沒有接收到值就預設為None,所以res=None
3、然後接著執行賦值語句後面的列印語句和if判斷,由於res為None所以執行count +=1,此時count值為1
4、再次遇到yield,返回1,並把foo函式掛起。
5、send函式是可以給yield生成器傳參的,執行send函式時會預設執行一次next函式,原理同上。
到這裡你可能就明白yield和return的關係和區別了,帶yield的函式是一個生成器,而不是一個函數了,這個生成器有一個函式就是next函式,next就相當於“下一步”生成哪個數,這一次的next開始的地方是接著上一次的next停止的地方執行的,所以呼叫next的時候,生成器並不會從foo函式的開始執行,只是接著上一步停止的地方開始,然後遇到yield後,return出要生成的數,此步就結束。
為什麼用這個生成器,是因為如果用List的話,會佔用更大的空間,比如說取0,1,2,3,4,5,6…………1000
你可能會這樣:
for i in range(1000):
print(i)
這個時候range(1000)就預設生成一個含有1000個數的list了,所以很佔記憶體。
這個時候你可以用剛才的yield組合成生成器進行實現:
但這個由於每次都要呼叫函式foo,所以比較耗時間。【這就是用時間換空間】
4、關鍵字:async/await
asyncio
是用來編寫
併發
程式碼的庫,使用
async/await
語法。
asyncio 被用作多個提供高效能 Python 非同步框架的基礎,包括網路和網站服務,資料庫連線庫,分散式任務佇列等等。
asyncio 往往是構建 IO 密集型和高層級 結構化 網路程式碼的最佳選擇。
正常的函式在執行時是不會中斷的,所以你要寫一個能夠中斷的函式,就需要新增async關鍵。
async 用來宣告一個函式為非同步函式
,非同步函式的特點是能在函式執行過程中掛起,去執行其他非同步函式,等到掛起條件(假設掛起條件是asyncio。sleep(5))消失後,也就是5秒到了再回來執行。
await 用來用來宣告程式掛起
,比如非同步程式執行到某一步時需要等待的時間很長,就將此掛起,去執行其他的非同步程式。await 後面只能跟非同步程式或有await屬性的物件,因為非同步程式與一般程式不同。假設有兩個非同步函式async a,async b,a中的某一步有await,當程式碰到關鍵字await b()後,非同步程式掛起後去執行另一個非同步b程式,就是從函式內部跳出去執行其他函式,當掛起條件消失後,不管b是否執行完,要馬上從b程式中跳出來,回到原程式執行原來的操作。如果await後面跟的b函式不是非同步函式,那麼操作就只能等b執行完再返回,無法在b執行的過程中返回。如果要在b執行完才返回,也就不需要用await關鍵字了,直接呼叫b函式就行。所以這就需要await後面跟的是非同步函數了。在一個非同步函式中,可以不止一次掛起,也就是可以用多個await。
可以使用async、await來實現協程的併發
,下面以一個爬蟲例子來說明:
相對來說還是使用
async執行效率高些
。