1 關於greenlet
greelet指的是使用一個任務調度器和一些生成器或者協程實現協作式用戶空間多線程的一種偽並發機制,即所謂的微線程。
greelet機制的主要思想是:生成器函數或者協程函數中的yield語句掛起函數的執行,直到稍后使用next()或send()操作進行恢復為止。可以使用一個調度器循環在一組生成器函數之間協作多個任務。
網絡框架的幾種基本的網絡I/O模型:
阻塞式單線程:這是最基本的I/O模型,只有在處理完一個請求之后才會處理下一個請求。它的缺點是效能差,如果有請求阻塞住,會讓服務無法繼續接受請求。但是這種模型編寫代碼相對簡單,在應對訪問量不大的情況時是非常適合的。
阻塞式多線程:針對於單線程接受請求量有限的缺點,一個很自然的想法就是給每一個請求開一個線程去處理。這樣做的好處是能夠接受更多的請求,缺點是在線程產生到一定數量之后,進程之間需要大量進行切換上下文的操作,會占用CPU大量的時間,不過這樣處理的話編寫代碼的難道稍高於單進程的情況。
非阻塞式事件驅動:為了解決多線程的問題,有一種做法是利用一個循環來檢查是否有網絡IO的事件發生,以便決定如何來進行處理(reactor設計模式)。這樣的做的好處是進一步降低了CPU的資源消耗。缺點是這樣做會讓程序難以編寫,因為請求接受后的處理過程由reactor來決定,使得程序的執行流程難以把握。當接受到一個請求后如果涉及到阻塞的操作,這個請求的處理就會停下來去接受另一個請求,程序執行的流程不會像線性程序那樣直觀。twisted框架就是應用這種IO模型的典型例子。
非阻塞式Coroutine(協程):這個模式是為了解決事件驅動模型執行流程不直觀的問題,它在本質上也是事件驅動的,加入了Coroutine的概念。
2 與線程/進程的區別
線程是搶占式的調度,多個線程並行執行,搶占共同的系統資源;而微線程是協同式的調度。
其實greenlet不是一種真正的並發機制,而是在同一線程內,在不同函數的執行代碼塊之間切換,實施“你運行一會、我運行一會”,並且在進行切換時必須指定何時切換以及切換到哪。greenlet的接口是比較簡單易用的,但是使用greenlet時的思考方式與其他並發方案存在一定區別:
1. 線程/進程模型在大邏輯上通常從並發角度開始考慮,把能夠並行處理的並且值得並行處理的任務分離出來,在不同的線程/進程下運行,然后考慮分離過程可能造成哪些互斥、沖突問題,將互斥的資源加鎖保護來保證並發處理的正確性。
2. greenlet則是要求從避免阻塞的角度來進行開發,當出現阻塞時,就顯式切換到另一段沒有被阻塞的代碼段執行,直到原先的阻塞狀況消失以后,再人工切換回原來的代碼段繼續處理。因此,greenlet本質是一種合理安排了的 串行 。
3. greenlet本質是串行,因此在沒有進行顯式切換時,代碼的其他部分是無法被執行到的,如果要避免代碼長時間占用運算資源造成程序假死,那么還是要將greenlet與線程/進程機制結合使用(每個線程、進程下都可以建立多個greenlet,但是跨線程/進程時greenlet之間無法切換或通訊)。
3 使用
一個 “greenlet” 是一個很小的獨立微線程。可以把它想像成一個堆棧幀,棧底是初始調用,而棧頂是當前greenlet的暫停位置。你使用greenlet創建一堆這樣的堆棧,然后在他們之間跳轉執行。跳轉不是絕對的:一個greenlet必須選擇跳轉到選擇好的另一個greenlet,這會讓前一個掛起,而后一個恢復。兩 個greenlet之間的跳轉稱為 切換(switch) 。
當你創建一個greenlet,它得到一個初始化過的空堆棧;當你第一次切換到它,他會啟動指定的函數,然后切換跳出greenlet。當最終棧底 函數結束時,greenlet的堆棧又編程空的了,而greenlet也就死掉了。greenlet也會因為一個未捕捉的異常死掉。
示例:來自官方文檔示例
from greenlet import greenlet
def test1():
print 12
gr2.switch()
print 34
def test2():
print 56
gr1.switch()
print 78
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
最后一行跳轉到 test1() ,它打印12,然后跳轉到 test2() ,打印56,然后跳轉回 test1() ,打印34,然后 test1() 就結束,gr1死掉。這時執行會回到原來的 gr1.switch() 調用。注意,78是不會被打印的,因為gr1已死,不會再切換。
4 基於greenlet的框架
4.1 eventlet
eventlet 是基於 greenlet 實現的面向網絡應用的並發處理框架,提供“線程”池、隊列等與其他 Python 線程、進程模型非常相似的 api,並且提供了對 Python 發行版自帶庫及其他模塊的超輕量並發適應性調整方法,比直接使用 greenlet 要方便得多。
其基本原理是調整 Python 的 socket 調用,當發生阻塞時則切換到其他 greenlet 執行,這樣來保證資源的有效利用。需要注意的是:
-
eventlet 提供的函數只能對 Python 代碼中的 socket 調用進行處理,而不能對模塊的 C 語言部分的 socket 調用進行修改。對后者這類模塊,仍然需要把調用模塊的代碼封裝在 Python 標准線程調用中,之后利用 eventlet 提供的適配器實現 eventlet 與標准線程之間的協作。
-
雖然 eventlet 把 api 封裝成了非常類似標准線程庫的形式,但兩者的實際並發執行流程仍然有明顯區別。在沒有出現 I/O 阻塞時,除非顯式聲明,否則當前正在執行的 eventlet 永遠不會把 cpu 交給其他的 eventlet,而標准線程則是無論是否出現阻塞,總是由所有線程一起爭奪運行資源。所有 eventlet 對 I/O 阻塞無關的大運算量耗時操作基本沒有什么幫助。
4.2 gevent
4.2.1 gevent是一個基於協程(coroutine)的Python網絡函數庫,通過使用greenlet提供了一個在libev事件循環頂部的高級別並發API。
主要特性有以下幾點:
-
基於libev的快速事件循環,Linux上面的是epoll機制
-
基於greenlet的輕量級執行單元
-
API復用了Python標准庫里的內容
-
支持SSL的協作式sockets
-
可通過線程池或c-ares實現DNS查詢
-
通過monkey patching功能來使得第三方模塊變成協作式
ps:
1、關於Linux的epoll機制:
epoll是Linux內核為處理大批量文件描述符而作了改進的poll,是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量並發連接中只有少量活躍的情況下的系統CPU利用率。epoll的優點:
-
支持一個進程打開大數目的socket描述符。select的一個進程所打開的FD由FD_SETSIZE的設置來限定,而epoll沒有這個限制,它所支持的FD上限是最大可打開文件的數目,遠大於2048。
-
IO效率不隨FD數目增加而線性下降:由於epoll只會對“活躍”的socket進行操作,於是,只有"活躍"的socket才會主動去調用 callback函數,其他idle狀態的socket則不會。
-
使用mmap加速內核與用戶空間的消息傳遞。epoll是通過內核於用戶空間mmap同一塊內存實現的。
-
內核微調。
2、libev機制
提供了指定文件描述符事件發生時調用回調函數的機制。libev是一個事件循環器:向libev注冊感興趣的事件,比如socket可讀事件,libev會對所注冊的事件的源進行管理,並在事件發生時觸發相應的程序。
4.2.2 官方文檔中的示例:
>>> import gevent
>>> from gevent import socket
>>> urls = ['www.google.com.hk','www.example.com', 'www.python.org' ]
>>> jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
>>> gevent.joinall(jobs, timeout=2)
>>> [job.value for job in jobs]
['74.125.128.199', '208.77.188.166', '82.94.164.162']
注解:gevent.spawn()方法spawn一些jobs,然后通過gevent.joinall將jobs加入到微線程執行隊列中等待其完成,設置超時為2秒。執行后的結果通過檢查gevent.Greenlet.value值來收集。gevent.socket.gethostbyname()函數與標准的socket.gethotbyname()有相同的接口,但它不會阻塞整個解釋器,因此會使得其他的greenlets跟隨着無阻的請求而執行。
4.2.3 Monket patching
Python的運行環境允許我們在運行時修改大部分的對象,包括模塊、類甚至函數。雖然這樣做會產生“隱式的副作用”,而且出現問題很難調試,但在需要修改Python本身的基礎行為時,Monkey patching就派上用場了。Monkey patching能夠使得gevent修改標准庫里面大部分的阻塞式系統調用,包括socket,ssl,threading和select等模塊,而變成協作式運行。
>>> from gevent import monkey ;
>>> monkey . patch_socket ()
>>> import urllib2
通過monkey.patch_socket()方法,urllib2模塊可以使用在多微線程環境,達到與gevent共同工作的目的。
4.2.4 事件循環
不像其他網絡庫,gevent和eventlet類似, 在一個greenlet中隱式開始事件循環。沒有必須調用run()或dispatch()的反應器(reactor),在twisted中是有 reactor的。當gevent的API函數想阻塞時,它獲得Hub實例(執行時間循環的greenlet),並切換過去。如果沒有集線器實例則會動態 創建。
libev提供的事件循環默認使用系統最快輪詢機制,設置LIBEV_FLAGS環境變量可指定輪詢機制。LIBEV_FLAGS=1為select, LIBEV_FLAGS = 2為poll, LIBEV_FLAGS = 4為epoll,LIBEV_FLAGS = 8為kqueue。
Libev的API位於gevent.core下。注意libev API的回調在Hub的greenlet運行,因此使用同步greenlet的API。可以使用spawn()和Event.set()等異步API。
