在學習gevent之前,你肯定要知道你學的這個東西是什麼。
官方描述gevent
gevent is a coroutine-based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.
翻譯:gevent是一個基於協程的Python網路庫。我們先理解這句,也是這次學習的重點——協程。
wiki描述協程
與子例程一樣,協程也是一種程式元件。相對子例程而言,協程更為一般和靈活,但在實踐中使用沒有子例程那樣廣泛。子例程的起始處是惟一的入口點,一旦退出即完成了子例程的執行,子例程的一個例項只會返回一次;協程可以通過yield來呼叫其它協程。通過yield方式轉移執行權的協程之間不是呼叫者與被呼叫者的關係,而是彼此對稱、平等的。協程允許多個入口點,可以在指定位置掛起和恢復執行。
沒看懂?沒關係,我也沒看懂,不過算是有點線索:子例程。
子例程
過程有兩種,一種叫子例程(Subroutine),通常叫Sub;另一種叫函式(Function)。底層實現機制是一樣的,區別在於,Sub只執行操作,沒有返回值;Function不但執行操作,並且有返回值。用過VB的應該會比較清楚這點。(原諒我用了百度百科)說到底子例程就是過程,我們一般叫它函式。
說到函式,我就想吐槽了,不明白為什麼要叫函式。很多時候我們寫一個函式是為了封裝、模組化某個功能,它是一個功能、或者說是一個過程。因為它包含的是類似於流程圖那樣的具體邏輯,先怎樣做,然後怎樣做;如果遇到A情況則怎樣,如果遇到B情況又怎樣。個人覺得還是叫過程比較好,叫做函式就讓人很糾結了,難道因為迴歸到底層還是計算問題,出於數學的角度把它稱為函式?這個略坑啊!為了符合大家的口味,我還是稱之為函式好了(其實我也習慣叫函式了%>_
講到函式,我們就往底層深入一點,看看下面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def a(): print "a start" b() print "a end" def b(): print "b start" c() print "b end" def c(): print "c start" print "c end" if __name__ == "__main__": a() a start b start c start c end b end a end |
對於這樣的結果大家肯定不會意外的。每當函式被呼叫,就會在棧中開闢一個棧空間,呼叫結束後再回收該空間。
假設一個這樣的場景:有個講臺,每個人都可以上去發表言論,但是每次講臺只能站一個人。現在a在上面演講,當他說到“大家好!”的時候,b有個緊急通知要告訴大家,所以a就先下來讓b講完通知,然後a再上講臺繼續演講。如果用函式的思想模擬這個問題,堆疊示意圖是這樣的:
大家會不會發現問題,就是b通知完a繼續演講都要重新開始。因為函式在重新呼叫的時候,它的區域性變數是會被重置的,對於之前他說的那句“大家好”,他是不會記得的(可能a的記性不好)。那有沒有什麼辦法可以不讓他重複,而是在打斷之後繼續呢?很簡單,在他走下講臺之前記住當前說過的話。表現在函式中就是在退出之前,儲存該函式的區域性變數,方便在重新進入該函式的時候,能夠從之前的區域性變數開始繼續執行。
升級版
如果你有一段程式碼生產資料,另外一段程式碼消費資料,哪個應該是呼叫者,哪個應該是被呼叫者?
例如:生產者 —— 消費者問題,先拋開程式、執行緒等實現方法。假設有兩個函式producer和consumer,當緩衝區滿了,producer呼叫consumer,當緩衝區空了,consumer呼叫producer,但是這樣的函式互相呼叫會出什麼問題?
1 2 3 4 5 6 7 8 |
def producer(): print "生產一個" consumer() def consumer(): print "消費一個" producer() |
producer生產一個,緩衝區滿了,consumer消費一個,緩衝區空了,producer生產一個,如此迴圈。會看到下面這樣的圖:
看起來好像不錯,感覺兩個函式協調執行的很好,很好的解決了生產者——消費者問題。如果真有這麼好也就不會有協程的存在了,仔細分析會有兩個問題:
- 無限次數的函式巢狀呼叫,而沒有函式返回,會有什麼樣的後果?
- 兩個函式貌似協調有序的工作,你來我往,但每次執行的都是同一個函式例項嗎?
首先,上面的虛擬碼示例是一個無限的函式巢狀呼叫,沒有函式返回來釋放棧,棧的空間不斷的在增長,直到溢位,程式崩潰。然後,看起來兩個函式協調有序,事實上操作的都不是同一個例項物件,不知道下面的圖能否看懂。
那什麼東西有這樣的能力呢?我們很快就可以想到程式、執行緒,但是你真的想使用程式、執行緒如此重量級的東西在這麼簡單的程式上嗎?野蠻的搶佔式機制和笨重的上下文切換!
還有一種程式元件,那就是協程。它能保留上一次呼叫時的狀態,每次重新進入該過程的時候,就相當於回到上一次離開時所處邏輯流的位置。協程的起始處是第一個入口點,在協程裡,返回點之後是接下來的入口點。協程的生命期完全由他們的使用的需要決定。每個協程在用yield命令向另一個協程交出控制時都儘可能做了更多的工作,放棄控制使得另一個協程從這個協程停止的地方開始,接下來的每次協程被呼叫時,都是從協程返回(或yield)的位置接著執行。
從上面這些你就可以知道其實協程是模擬了多執行緒(或多程式)的操作,多執行緒在切換的時候都會有一個上下文切換,在退出的時候將現場儲存起來,等到下一次進入的時候從儲存的現場開始,繼續執行。
看下協程是怎樣實現的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import random from time import sleep from greenlet import greenlet from Queue import Queue queue = Queue(1) @greenlet def producer(): chars = ['a', 'b', 'c', 'd', 'e'] global queue while True: char = random.choice(chars) queue.put(char) print "Produced: ", char sleep(1) consumer.switch() @greenlet def consumer(): global queue while True: char = queue.get() print "Consumed: ", char sleep(1) producer.switch() if __name__ == "__main__": producer.run() consumer.run() |
應用場景
我們一直都在大談協程是什麼樣一個東西,卻從沒有提起協程用來幹嘛,這個其實大家分析一下就能夠知道。從上面的生產者——消費者問題應該能看出,它分別有兩個任務,假設交給兩個人去執行,但每次只能允許一個人行動。當緩衝區滿的時候,生產者是出於等待狀態的,這個時候可以將執行任務的權利轉交給消費者,當緩衝區空得時候,消費者是出於等待狀態的,這個時候可以將執行任務的權利轉交給生產者,是不是很容易聯想到多工切換?然後想到執行緒?最後想到高併發?
但同學們又會問,既然有了執行緒為什麼還要協程呢?因為執行緒是系統級別的,在做切換的時候消耗是特別大的,具體為什麼這麼大等我研究好了再告訴你;同時執行緒的切換是由CPU決定的,可能你剛好執行到一個地方的時候就要被迫終止,這個時候你需要用各種措施來保證你的資料不出錯,所以執行緒對於資料安全的操作是比較複雜的。而協程是使用者級別的切換,且切換是由自己控制,不受外力終止。
總結
協程其實模擬了人類活動的一種過程。例如:你準備先寫文件,然後修復bug。這時候接到電話說這個bug很嚴重,必須立即修復(可以看作CPU通知)。於是你暫停寫文件,開始去填坑,終於你把坑填完了,你回來寫文件,這個時候你肯定是接著之前寫的文件繼續,難道你要把之前寫的給刪了,重新寫?這就是協程。那如果是子例程呢?那你就必須重新寫了,因為退出之後,棧幀就會被彈出銷燬,再次呼叫就是開闢新的棧空間了。
總結:協程就是使用者態下的執行緒,是人們在有了程式、執行緒之後仍覺得效率不夠,而追求的又一種高併發解決方案。為什麼說是使用者態,是因為作業系統並不知道它的存在,它是由程式設計師自己控制、互相協作的讓出控制權而不是像程式、執行緒那樣由作業系統排程決定是否讓出控制權。