Python並發編程協程(Coroutine)之Gevent


Gevent官網文檔地址:http://www.gevent.org/contents.html

基本概念

我們通常所說的協程Coroutine其實是corporate routine的縮寫,直接翻譯為協同的例程,一般我們都簡稱為協程。

在linux系統中,線程就是輕量級的進程,而我們通常也把協程稱為輕量級的線程即微線程。

進程和協程

下面對比一下進程和協程的相同點和不同點:

相同點:
我們都可以把他們看做是一種執行流,執行流可以掛起,並且后面可以在你掛起的地方恢復執行,這實際上都可以看做是continuation,關於這個我們可以通過在linux上運行一個hello程序來理解:

shell進程和hello進程:

  1. 開始,shell進程在運行,等待命令行的輸入
  2. 執行hello程序,shell通過系統調用來執行我們的請求,這個時候系統調用會講控制權傳遞給操作系統。操作系統保存shell進程的上下文,創建一個hello進程以及其上下文並將控制權給新的hello進程。
  3. hello進程終止后,操作系統恢復shell進程的上下文,並將控制權傳回給shell進程
  4. shell進程繼續等待下個命令的輸入

當我們掛起一個執行流的時,我們要保存的東西:

  1. 棧, 其實在你切換前你的局部變量,以及要函數的調用都需要保存,否則都無法恢復
  2. 寄存器狀態,這個其實用於當你的執行流恢復后要做什么

而寄存器和棧的結合就可以理解為上下文,上下文切換的理解:
CPU看上去像是在並發的執行多個進程,這是通過處理器在進程之間切換來實現的,操作系統實現這種交錯執行的機制稱為上下文切換

操作系統保持跟蹤進程運行所需的所有狀態信息。這種狀態,就是上下文。
在任何一個時刻,操作系統都只能執行一個進程代碼,當操作系統決定把控制權從當前進程轉移到某個新進程時,就會進行上下文切換,即保存當前進程的上下文,恢復新進程的上下文,然后將控制權傳遞到新進程,新進程就會從它上次停止的地方開始。

不同點:

  1. 執行流的調度者不同,進程是內核調度,而協程是在用戶態調度,也就是說進程的上下文是在內核態保存恢復的,而協程是在用戶態保存恢復的,很顯然用戶態的代價更低
  2. 進程會被強占,而協程不會,也就是說協程如果不主動讓出CPU,那么其他的協程,就沒有執行的機會。
  3. 對內存的占用不同,實際上協程可以只需要4K的棧就足夠了,而進程占用的內存要大的多
  4. 從操作系統的角度講,多協程的程序是單進程,單協程

線程和協程

既然我們上面也說了,協程也被稱為微線程,下面對比一下協程和線程:

  1. 線程之間需要上下文切換成本相對協程來說是比較高的,尤其在開啟線程較多時,但協程的切換成本非常低。
  2. 同樣的線程的切換更多的是靠操作系統來控制,而協程的執行由我們自己控制

我們通過下面的圖更容易理解:

從上圖可以看出,協程只是在單一的線程里不同的協程之間切換,其實和線程很像,線程是在一個進程下,不同的線程之間做切換,這也可能是協程稱為微線程的原因吧

繼續分析協程:

Gevent

Gevent是一種基於協程的Python網絡庫,它用到Greenlet提供的,封裝了libevent事件循環的高層同步API。它讓開發者在不改變編程習慣的同時,用同步的方式寫異步I/O的代碼。

使用Gevent的性能確實要比用傳統的線程高,甚至高很多。但這里不得不說它的一個坑:

  1. Monkey-patching,我們都叫猴子補丁,因為如果使用了這個補丁,Gevent直接修改標准庫里面大部分的阻塞式系統調用,包括socket、ssl、threading和 select等模塊,而變為協作式運行。但是我們無法保證你在復雜的生產環境中有哪些地方使用這些標准庫會由於打了補丁而出現奇怪的問題
  2. 第三方庫支持。得確保項目中用到其他用到的網絡庫也必須使用純Python或者明確說明支持Gevent

既然Gevent用的是Greenlet,我們通過下圖來理解greenlet:

每個協程都有一個parent,最頂層的協程就是man thread或者是當前的線程,每個協程遇到IO的時候就把控制權交給最頂層的協程,它會看那個協程的IO event已經完成,就將控制權給它。

下面是greenlet一個例子

 1 from greenlet import greenlet
 2 
 3 def test1(x,y):
 4     z = gr2.switch(x+y)
 5     print(z)
 6 
 7 
 8 def test2(u):
 9     print(u)
10     gr1.switch(42)
11 
12 
13 gr1 = greenlet(test1)
14 gr2 = greenlet(test2)
15 
16 
17 gr1.switch("hello",'world')

greenlet(run=None, parent=None): 創建一個greenlet實例.
gr.parent:每一個協程都有一個父協程,當前協程結束后會回到父協程中執行,該 屬性默認是創建該協程的協程.
gr.run: 該屬性是協程實際運行的代碼. run方法結束了,那么該協程也就結束了.
gr.switch(*args, **kwargs): 切換到gr協程.
gr.throw(): 切換到gr協程,接着拋出一個異常.

下面是gevent的一個例子:

 1 import gevent
 2 
 3 def func1():
 4     print("start func1")
 5     gevent.sleep(1)
 6     print("end func1")
 7 
 8 
 9 def func2():
10     print("start func2")
11     gevent.sleep(1)
12     print("end func2")
13 
14 gevent.joinall(
15     [
16         gevent.spawn(func1),
17         gevent.spawn(func2)
18     ]
19 )

關於gevent中隊列的使用

gevent中也有自己的隊列,但是有一個場景我用的過程中發現一個問題,就是如果我在協程中通過這個q來傳遞數據,如果對了是空的時候,從隊列獲取數據的那個協程就會被切換到另外一個協程中,這個協程用於往隊列里put放入數據,問題就出在,gevent不認為這個放入數據為IO操作,並不會切換到上一個協程中,會把這個協程的任務完成后在切換到另外一個協程。我原本想要實現的效果是往對了放入數據后就會切換到get的那個協程。(或許我這里理解有問題)下面是測試代碼:

 1 import gevent
 2 from gevent.queue import Queue
 3 
 4 
 5 def func():
 6     for i in range(10):
 7 
 8         print("int the func")
 9         q.put("test")
10 
11 def func2():
12     for i in range(10):
13         print("int the func2")
14         res = q.get()
15         print("--->",res)
16 
17 q = Queue()
18 gevent.joinall(
19     [
20         gevent.spawn(func2),
21         gevent.spawn(func),
22     ]
23 )

這段代碼的運行效果為:

如果我在fun函數的q.put("test")后面添加gevent.sleep(0),就會是如下效果:

原本我預測的在不修改代碼的情況下就應該是第二個圖的結果,但是實際卻是第一個圖的結果(這個問題可能是我自己沒研究明白,后面繼續研究)

關於Gevent的問題

就像我上面說的gevent和第三方庫配合使用會有一些問題,可以總結為:
python協程的庫可以直接monkey path 
C寫成的庫可以采用豆瓣開源的greenify來打patch(這個功能自己准備后面做測試)

不過總的來說gevent目前為止還是有很多缺陷,並且不是官網標准庫,而在python3中有一個官網正在做並且在3.6中已經穩定的庫asyncio,這也是一個非常具有野心的庫,非常建議學習,我也准備后面深入了解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM