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

轉載自https://www.cnblogs.com/zhaof/p/7536569.htmlhtml

 

 

event官網文檔地址:http://www.gevent.org/contents.htmlpython

基本概念

咱們一般所說的協程Coroutine實際上是corporate routine的縮寫,直接翻譯爲協同的例程,通常咱們都簡稱爲協程。linux

在linux系統中,線程就是輕量級的進程,而咱們一般也把協程稱爲輕量級的線程即微線程。shell

進程和協程

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

相同點:
咱們均可以把他們看作是一種執行流,執行流能夠掛起,而且後面能夠在你掛起的地方恢復執行,這實際上均可以看作是continuation,關於這個咱們能夠經過在linux上運行一個hello程序來理解:網絡

shell進程和hello進程:併發

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

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

  1. 棧, 其實在你切換前你的局部變量,以及要函數的調用都須要保存,不然都沒法恢復
  2. 寄存器狀態,這個其實用於當你的執行流恢復後要作什麼

而寄存器和棧的結合就能夠理解爲上下文,上下文切換的理解:
CPU看上去像是在併發的執行多個進程,這是經過處理器在進程之間切換來實現的,操做系統實現這種交錯執行的機制稱爲上下文切換socket

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

不一樣點:

  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,這也是一個很是具備野心的庫,很是建議學習,我也準備後面深刻了解

全部的努力都值得期許,每一份夢想都應該灌溉!
 
分類:  python成長之路
標籤:  python併發編程
相關文章
相關標籤/搜索