python之協程
一 引子
本節的主題是基於單線程來實現併發,即只用一個主線程(很明顯可利用的cpu只有一個)狀況下實現併發,爲此咱們須要先回顧下併發的本質:切換+保存狀態python
cpu正在運行一個任務,會在兩種狀況下切走去執行其餘的任務(切換由操做系統強制控制),一種狀況是該任務發生了阻塞,另一種狀況是該任務計算的時間過長或有一個優先級更高的程序替代了它nginx
協程本質上就是一個線程,之前線程任務的切換是由操做系統控制的,遇到I/O自動切換,如今咱們用協程的目的就是較少操做系統切換的開銷(開關線程,建立寄存器、堆棧等,在他們之間進行切換等),在咱們本身的程序裏面來控制任務的切換。git
ps:在介紹進程理論時,說起進程的三種執行狀態,而線程纔是執行單位,因此也能夠將上圖理解爲線程的三種狀態 github
一:其中第二種狀況並不能提高效率,只是爲了讓cpu可以雨露均沾,實現看起來全部任務都被「同時」執行的效果,若是多個任務都是純計算的,這種切換反而會下降效率。爲此咱們能夠基於yield來驗證。yield自己就是一種在單線程下能夠保存任務運行狀態的方法,咱們來簡單複習一下:編程
#1 yiled能夠保存狀態,yield的狀態保存與操做系統的保存線程狀態很像,可是yield是代碼級別控制的,更輕量級 #2 send能夠把一個函數的結果傳給另一個函數,以此實現單線程內程序之間的切換
二:第一種狀況的切換。在任務一遇到io狀況下,切到任務二去執行,這樣就能夠利用任務一阻塞的時間完成任務二的計算,效率的提高就在於此。網絡
協程就是告訴Cpython解釋器,你不是nb嗎,不是搞了個GIL鎖嗎,那好,我就本身搞成一個線程讓你去執行,省去你切換線程的時間,我本身切換比你切換要快不少,避免了不少的開銷,對於單線程下,咱們不可避免程序中出現io操做,但若是咱們能在本身的程序中(即用戶程序級別,而非操做系統級別)控制單線程下的多個任務能在一個任務遇到io阻塞時就切換到另一個任務去計算,這樣就保證了該線程可以最大限度地處於就緒態,即隨時均可以被cpu執行的狀態,至關於咱們在用戶程序級別將本身的io操做最大限度地隱藏起來,從而能夠迷惑操做系統,讓其看到:該線程好像是一直在計算,io比較少,從而更多的將cpu的執行權限分配給咱們的線程。多線程
協程的本質就是在單線程下,由用戶本身控制一個任務遇到io阻塞了就切換另一個任務去執行,以此來提高效率。爲了實現它,咱們須要找尋一種能夠同時知足如下條件的解決方案:併發
#1. 能夠控制多個任務之間的切換,切換以前將任務的狀態保存下來,以便從新運行時,能夠基於暫停的位置繼續執行。 #2. 做爲1的補充:能夠檢測io操做,在遇到io操做的狀況下才發生切換
二 協程介紹
協程:是單線程下的併發,又稱微線程,纖程。英文名Coroutine。一句話說明什麼是線程:協程是一種用戶態的輕量級線程,即協程是由用戶程序本身控制調度的。、負載均衡
須要強調的是:
#1. python的線程屬於內核級別的,即由操做系統控制調度(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其餘線程運行) #2. 單線程內開啓協程,一旦遇到io,就會從應用程序級別(而非操做系統)控制切換,以此來提高效率(!!!非io操做的切換與效率無關)
對比操做系統控制線程的切換,用戶在單線程內控制協程的切換
優勢以下:
#1. 協程的切換開銷更小,屬於程序級別的切換,操做系統徹底感知不到,於是更加輕量級 #2. 單線程內就能夠實現併發的效果,最大限度地利用cpu
缺點以下:
#1. 協程的本質是單線程下,沒法利用多核,能夠是一個程序開啓多個進程,每一個進程內開啓多個線程,每一個線程內開啓協程 #2. 協程指的是單個線程,於是一旦協程出現阻塞,將會阻塞整個線程
總結協程特色:
- 必須在只有一個單線程裏實現併發
- 修改共享數據不需加鎖
- 用戶程序裏本身保存多個控制流的上下文棧
- 附加:一個協程遇到IO操做自動切換到其它協程(如何實現檢測IO,yield、greenlet都沒法實現,就用到了gevent模塊(select機制))
三 Greenlet
若是咱們在單個線程內有20個任務,要想實如今多個任務之間切換,使用yield生成器的方式過於麻煩(須要先獲得初始化一次的生成器,而後再調用send。。。很是麻煩),而使用greenlet模塊能夠很是簡單地實現這20個任務直接的切換
#安裝 pip3 install greenlet
from greenlet import greenlet def eat(name): print('%s eat 1' %name) #2 g2.switch('taibai') #3 print('%s eat 2' %name) #6 g2.switch() #7 def play(name): print('%s play 1' %name) #4 g1.switch() #5 print('%s play 2' %name) #8 g1=greenlet(eat) g2=greenlet(play) g1.switch('taibai')#能夠在第一次switch時傳入參數,之後都不須要 1
單純的切換(在沒有io的狀況下或者沒有重複開闢內存空間的操做),反而會下降程序的執行速度
greenlet只是提供了一種比generator更加便捷的切換方式,當切到一個任務執行時若是遇到io,那就原地阻塞,仍然是沒有解決遇到IO自動切換來提高效率的問題。
上面這個圖,是協程真正的意義,雖然沒有規避固有的I/O時間,可是咱們使用這個時間來作別的事情了,通常在工做中咱們都是進程+線程+協程的方式來實現併發,以達到最好的併發效果,若是是4核的cpu,通常起5個進程,每一個進程中20個線程(5倍cpu數量),每一個線程能夠起500個協程,大規模爬取頁面的時候,等待網絡延遲的時間的時候,咱們就能夠用協程去實現併發。 併發數量 = 5 * 20 * 500 = 50000個併發,這是通常一個4cpu的機器最大的併發數。nginx在負載均衡的時候最大承載量就是5w個
單線程裏的這20個任務的代碼一般會既有計算操做又有阻塞操做,咱們徹底能夠在執行任務1時遇到阻塞,就利用阻塞的時間去執行任務2。。。。如此,才能提升效率,這就用到了Gevent模塊。
四 Gevent介紹
#安裝 pip3 install gevent
Gevent 是一個第三方庫,能夠輕鬆經過gevent實現併發同步或異步編程,在gevent中用到的主要模式是Greenlet, 它是以C擴展模塊形式接入Python的輕量級協程。 Greenlet所有運行在主程序操做系統進程的內部,但它們被協做式地調度。
#用法 g1=gevent.spawn(func,1,2,3,x=4,y=5)建立一個協程對象g1,spawn括號內第一個參數是函數名,如eat,後面能夠有多個參數,能夠是位置實參或關鍵字實參,都是傳給函數eat的,spawn是異步提交任務 g2=gevent.spawn(func2) g1.join() #等待g1結束 g2.join() #等待g2結束 有人測試的時候會發現,不寫第二個join也能執行g2,是的,協程幫你切換執行了,可是你會發現,若是g2裏面的任務執行的時間長,可是不寫join的話,就不會執行完等到g2剩下的任務了
#或者上述兩步合做一步:gevent.joinall([g1,g2]) g1.value#拿到func1的返回值
遇到IO阻塞時會自動切換任務
上例gevent.sleep(2)模擬的是gevent能夠識別的io阻塞,
而time.sleep(2)或其餘的阻塞,gevent是不能直接識別的須要用下面一行代碼,打補丁,就能夠識別了
from gevent import monkey;monkey.patch_all()必須放到被打補丁者的前面,如time,socket模塊以前
或者咱們乾脆記憶成:要用gevent,須要將from gevent import monkey;monkey.patch_all()放到文件的開頭
from gevent import monkey;monkey.patch_all() #必須寫在最上面,這句話後面的全部阻塞所有可以識別了 import gevent #直接導入便可 import time def eat():
#print() print('eat food 1') time.sleep(2) #加上mokey就可以識別到time模塊的sleep了 print('eat food 2') def play(): print('play 1') time.sleep(1) #來回切換,直到一個I/O的時間結束,這裏都是咱們個gevent作得,再也不是控制不了的操做系統了。 print('play 2') g1=gevent.spawn(eat) g2=gevent.spawn(play_phone) gevent.joinall([g1,g2]) print('主')
咱們能夠用threading.current_thread().getName()來查看每一個g1和g2,查看的結果爲DummyThread-n,即假線程,虛擬線程,其實都在一個線程裏面
進程線程的任務切換是由操做系統自行切換的,你本身不能控制
協程是經過本身的程序(代碼)來進行切換的,本身可以控制,只有遇到協程模塊可以識別的IO操做的時候,程序纔會進行任務切換,實現併發效果,若是全部程序都沒有IO操做,那麼就基本屬於串行執行了。
五 Gevent之同步與異步
六 Gevent之應用舉例一
將上面的程序最後加上一段串行的代碼看看效率:若是你的程序不須要過高的效率,那就不用什麼併發啊協程啊之類的東西。
print('--------------------------------') s = time.time() requests.get('https://www.python.org/') requests.get('https://www.yahoo.com/') requests.get('https://github.com/') t = time.time() print('串行時間>>',t-s)
七 Gevent之應用舉例二
經過gevent實現單線程下的socket併發(from gevent import monkey;monkey.patch_all()必定要放到導入socket模塊以前,不然gevent沒法識別socket的阻塞)
一個網絡請求裏面通過多個時間延遲time