Gevent的協程,能夠非同步是為什麼呢?
廖雪峰大大的Python教程網站上,關於Gevent,有這樣一段
```
from gevent import monkey; monkey.patch_all()
import gevent
import urllib2def f(url):
print("GET: %s" % url)
resp = urllib2.urlopen(url)
data = resp.read()
print("%d bytes received from %s." % (len(data), url))gevent.joinall([
gevent.spawn(f, "https://www.python.org/"),
gevent.spawn(f, "https://www.yahoo.com/"),
gevent.spawn(f, "https://github.com/"),
])
運行結果:
GET: https://www.python.org/
GET: https://www.yahoo.com/
GET: https://github.com/
45661 bytes received from https://www.python.org/.
14823 bytes received from https://github.com/.
304034 bytes received from https://www.yahoo.com/.
從結果看,3個網路操作是並發執行的,而且結束順序不同,但只有一個線程。
```我不理解,為什麼在同一個線程中,因為有協程就可以並發的訪問3個網站呢?
不知道題主用過非同步IO嘛,你在一個線程里弄三個非同步IO的請求,這三個請求一樣是並發的呀,Gevent只是方便你不用註冊Callback直接用同步的方式寫代碼而已。建議題主去看看epoll的實現,感覺你的疑問並不在協程上。
簡單類比成操縱系統調用,阻塞io/Sleep都會放到一個"系統線程(Hub)" 中.詳細參考 A Curious Course on Coroutines and Concurrency
要搞清楚這個問題,需要搞清楚協程(Coroutines),其它的看gevent 文檔Gevent Tutorial
協程(Coroutines)
與子常式一樣,協程也是一種程序組件。相對子常式而言,協程更為一般和靈活,但在實踐中使用沒有子常式那樣廣泛。協程源自Simula和Modula-2語言,但也有其他語言支持。協程更適合於用來實現彼此熟悉的程序組件,如合作式多任務,迭代器,無限列表和管道。
協程最初在1963年被提出。
Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, exceptions, event loop, iterators, infinite lists and pipes.
According to Donald Knuth, the term coroutine was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program.
[1]
The first published explanation of the coroutine appeared later, in 1963.
[2]
由於協程不如子常式那樣被普遍所知,最好對它們作個比較。
子常式的起始處是惟一的入口點,一旦退出即完成了子常式的執行,子常式的一個實例只會返回一次。
協程可以通過yield來調用其它協程。通過yield方式轉移執行權的協程之間不是調用者與被調用者的關係,而是彼此對稱、平等的。
協程的起始處是第一個入口點,在協程里,返回點之後是接下來的入口點。子常式的生命期遵循後進先出(最後一個被調用的子常式最先返回);相反,協程的生命期完全由他們的使用的需要決定。
這裡是一個簡單的例子證明協程的實用性。假設你有一個生產者-消費者的關係,這裡一個協程生產產品並將它們加入隊列,另一個協程從隊列中取出產品並使用它。為了提高效率,你想一次增加或刪除多個產品。代碼可能是這樣的:
var q := new queue
生產者協程
loop
while q is not full
create some new items
add the items to q
yield to consume
消費者協程
loop
while q is not empty
remove some items from q
use the items
yield to produce
每個協程在用yield命令向另一個協程交出控制時都儘可能做了更多的工作。放棄控制使得另一個常式從這個常式停止的地方開始,但因為現在隊列被修改了所以他可以做更多事情。儘管這個例子常用來介紹多線程,實際沒有必要用多線程實現這種動態:yield語句可以通過由一個協程向另一個協程直接分支的方式實現。
因為相對於子常式,協程可以有多個入口和出口點,可以用協程來實現任何的子常式。事實上,正如Knuth所說:「子常式是協程的特例。」
每當子常式被調用時,執行從被調用子常式的起始處開始;然而,接下來的每次協程被調用時,從協程返回(或yield)的位置接著執行。
因為子常式只返回一次,要返回多個值就要通過集合的形式。這在有些語言,如Forth里很方便;而其他語言,如C,只允許單一的返回值,所以就需要引用一個集合。相反地,因為協程可以返回多次,返回多個值只需要在後繼的協程調用中返回附加的值即可。在後繼調用中返回附加值的協程常被稱為產生器。
子常式容易實現於堆棧之上,因為子常式將調用的其他子常式作為下級。相反地,協程對等地調用其他協程,最好的實現是用continuations(由有垃圾回收的堆實現)以跟蹤控制流程。
協程有助於實現:
- 狀態機:在一個子常式里實現狀態機,這裡狀態由該過程當前的出口/入口點確定;這可以產生可讀性更高的代碼。
- 角色模型:並行的角色模型,例如計算機遊戲。每個角色有自己的過程(這又在邏輯上分離了代碼),但他們自願地向順序執行各角色過程的中央調度器交出控制(這是合作式多任務的一種形式)。
- 產生器:它有助於輸入/輸出和對數據結構的通用遍歷。
如果前提是只能有一個線程,那麼顯而易見地能夠想到的方法就是resp.read這個非同步操作會告訴你他要卡死了,然後你註冊個回調,暫停自己,然後調度走。等到回調結束了,再標記一下這個task已經能用了,坐等被調度。
這就像.net的Task&gevent內部還是封裝的libev非同步事件循環。所以,要理解gevent,首先需要對linux的非同步I/O有個概念。 舉個不恰當的例子,打開一個網頁,需要cpu向網卡發送指令,如果是阻塞情況,cpu會一直等待直到網卡通過網路獲取到了網頁內容,才會繼續工作,這樣整個工作都是序列化的,並且等待的時間啥都做不了,很顯然浪費了cpu。非同步的情況則是cpu把指令成功發送給網卡並註冊了回調函數以後,就直接返回做其他事情(cpu的利用率上去了,並且程序的執行非同步化了),對應於這個問題,就是cpu向網卡提交了一個網頁鏈接訪問請求以後,不等待網卡就繼續向網卡提交下一個網頁請求(網卡本身也是有控制器的,可以很好的並發處理多個網頁請求)。在網卡獲取到網頁內容以後,通過中斷機制告訴cpu,網頁獲取成功了。cpu接收到通知以後,可以中斷當前的執行流程去執行之前註冊的回調函數(由於網卡是並發訪問網頁的,每個網頁返回的時間不同導致了回調函數的順序發生變化)。 不過非同步最大的缺陷就是回調太多,程序的執行流程和中間狀態保存比較麻煩。而gevent是在這個基礎上,以一種同步的方式解決了回調帶來的問題。這樣更易於編寫和維護。
概念上和內核的線程調度一樣,不過gevent是在用戶態完成這件事的。
推薦讀一下這個項目https://github.com/dabeaz/curio 你就大概知道是怎麼回事了。
當然,這只是概念性的了解他是怎麼工作的。從題主最後一句話來看並沒有了解什麼是協程。
協程別名叫微線程,即只有一個線程。 可以簡單理解為單線程,但是擁有自己的寄存器上下文和棧,在不同的任務上切換。
況且cpython又有Global interpreter lock 的存在所以不可能是真正的並發。所以訪問的順序還是寫的列表的順序。但得到結果是順序就看誰快了。
IO多路復用
目前為了解決高並發的問題主要有多進程,多線程和協程三種方法。
我喜歡用這個例子來解釋協程:拿煎牛排為例子,一個牛排一面需要煎三分鐘分鐘,兩面需要煎六分鐘。多進程模式:一個廚房(進程)裡面有一個廚師(程序),一口鍋,一個廚房每6分鐘才能生產出一個牛排,n個進程每6分鐘能生產n個牛排。多線程模式:一個廚房有x個鍋,每個鍋都對應一個廚師(線程),也就是一個廚房每6分鐘能生產x個牛排,如果採用多進程+多線程,有Y個廚房,每6分鐘就能生產x*Y個牛排。多線程的方式貌似解決了高並發的問題,但是,當一個廚師煎牛排一面的3分鐘里,只能等著,什麼事情也做不了(阻塞),3分鐘後翻面,然後又需要等待3分鐘才能生產一個牛排。這不是浪費時間浪費資源嗎?協程就解決了這個問題,在一個廚房下,放入n口鍋,只有一個廚師(單線程),廚師在第一口鍋放入牛排後,等待3分鐘的過程中可以在其他鍋里放入牛排,如果忽略翻面的時間,一個廚房,一個廚師,每6分鐘能生產n個牛排。假設翻面+放入牛排耗時1s的話,7分鐘能生產牛排60個!!10分鐘能生產牛排240個!!!也就是說,制約程序性能的,有可能是等待(阻塞),等待牛排煎熟的過程,等待從硬碟讀取數據到內存的過程,等待資料庫sql查詢的過程,等待www.baidu/com下載的過程,如果網速慢,打開百度需要3s的話,為何不在等待這3s的時間再去打開下一個網頁,那個網頁先打開,我們就先看那個網頁,沒必要打開一個看一個。這就是協程的原理。第一次正經的在知乎上回答問題,手機碼字,本人也是小白一個,歡迎指正!
其實和goto的意思有點像…
Linux下有個epoll你造嘛
Python3.5已經直接支持協程了。你需要考慮的不是Gevent,而是弄明白協程是個什麼東西。知乎上自己搜一下。
python2和3無論是第三方庫還是3.5新增的語法都是偽協程
推薦閱讀: