天天看點

python 協程簡介

python協程、greenlet、gevent子產品簡介

之前我們了解了線程、程序的概念,了解了在作業系統中程序是資源配置設定的最小機關,線程是cpu排程的最小機關。按道理來說我們已經算是把cpu的使用率提高很多了。但是我們知道無論是建立多程序還是建立多線程,都要消耗一定的時間來建立程序、線程、以及管理他們之間的切換。随着我們對于效率的追求不斷提高,基于單線程來實作并發又成為一個新的課題,即隻用一個主線程(很明顯可利用的cpu隻有一個)情況下實作并發。這樣就可以節省建立線程序所消耗的時間。為此我們需要先回顧下并發的本質:切換+儲存狀态。

cpu正在運作一個任務,會在兩種情況下切走去執行其他的任務(切換由作業系統強制控制),一種情況是該任務發生了阻塞,另外一種情況是該任務計算的時間過長。

好了知道規律,我們就可以想一個辦法來欺騙作業系統,那如何欺騙呢?就是欺騙作業系統我一直處于很忙的狀态,這樣程式便一直處于就緒和執行的狀态。這也就是協程的本質,程式隻在就緒和執行狀态,而不在阻塞狀态。進而提高程式被cpu執行的機會。

下面我們使用yield生成器來騙作業系統:

對于單線程下,我們不可避免程式中出現io操作,但如果我們能在自己的程式中(即使用者程式級别,而非作業系統級别)控制單線程下的多個任務能在一個任務遇到io阻塞時就切換到另外一個任務去計算,這樣就保證了該線程能夠最大限度地處于就緒态,即随時都可以被cpu執行的狀态,相當于我們在使用者程式級别将自己的io操作最大限度地隐藏起來,進而可以迷惑作業系統,讓其看到:該線程好像是一直在計算,io比較少,進而更多的将cpu的執行權限配置設定給我們的線程。

協程的本質就是在單線程下,由使用者自己控制一個任務遇到io阻塞了就切換另外一個任務去執行,程式一直處于就緒和執行的狀态,欺騙操作業系統我這個程式沒有io阻塞,這樣就省去了程式遇到io時被挂起進入阻塞狀态,以此來提升效率。為了實作它,我們需要找尋一種可以同時滿足以下條件的解決方案:

可以控制多個任務之間的切換,切換之前将任務的狀态儲存下來,以便重新運作時,可以基于暫停的位置繼續執行。

可以檢測io操作,在遇到io操作的情況下才發生切換。

協程:是單線程下的并發,又稱微線程,纖程。英文名coroutine。協程是一種使用者态的輕量級線程,即協程是由使用者程式自己控制排程的。

需要強調的是:

python的線程屬于核心級别的,即由作業系統控制排程(如單線程遇到io或執行時間過長就會被迫交出cpu執行權限,切換其他線程運作)。

單線程内開啟協程,任務排程由使用者控制,一旦遇到io,就會切換到其它任務,以此來提升效率,對比作業系統控制線程的切換,使用者在單線程内控制協程的切換。

優點如下:

協程的切換開銷更小,屬于程式級别的切換,作業系統完全感覺不到,因而更加輕量級。

單線程内就可以實作并發的效果,最大限度地利用cpu。

缺點如下:

協程的本質是單線程下,無法利用多核,可以是一個程式開啟多個程序,每個程序内開啟多個線程,每個線程内開啟協程。

協程指的是單個線程,因而一旦協程出現阻塞,将會阻塞整個線程。

協程特點:

在單線程裡實作并發。

修改共享資料不需加鎖。

任務的上下文切換由使用者控制。

附加:一個協程遇到io操作自動切換到其它協程(如何實作檢測io,yield、greenlet都無法實作,就用到了gevent子產品(select機制))

單純的切換(在沒有io的情況下或者沒有重複開辟記憶體空間的操作),反而會降低程式的執行速度,如下對比:

使用切換的方式如下:

使用正常串行如下:

上面的代碼是純計算的,通過對比可以看出串行的速度比協程的速度要快很多。是以協程一般适用于多i/o的情況。

gevent 是一個第三方庫,可以輕松通過gevent實作并發同步或異步程式設計,在gevent中用到的主要模式是greenlet, 它是以c擴充子產品形式接入python的輕量級協程。 greenlet全部運作在主程式作業系統程序的内部,但它們被協作式地排程。

上例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()放到檔案的開頭

我們可以用threading.current_thread().getname()來檢視每個g1和g2,檢視的結果為dummythread-n,即假線程  dummy虛拟的,假的。

上面程式的重要部分是将task函數封裝到greenlet内部線程的gevent.spawn。

初始化的greenlet清單存放在數組threads中,此數組被傳給gevent.joinall 函數,後者阻塞目前流程,并執行所有給定的greenlet任務。執行流程隻會在 所有greenlet執行完後才會繼續向下走。

gevent應用舉例: