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功能來使得第三方子產品變成協作式
4.2.2 官方文檔中的示例:
>>> import gevent
>>> from gevent import socket
>>> urls = [‘ ‘ ‘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。
——遊響雲停
本文出自 “” 部落格,請務必保留此出處