Python 协程gevent
gevent是第三方庫(kù),通過(guò)greenlet實(shí)現(xiàn)協(xié)程,其基本思想是:
當(dāng)一個(gè)greenlet遇到IO操作時(shí),比如訪問(wèn)網(wǎng)絡(luò),就自動(dòng)切換到其他的greenlet,等到IO操作完成,再在適當(dāng)?shù)臅r(shí)候切換回來(lái)繼續(xù)執(zhí)行。由于IO操作非常耗時(shí),經(jīng)常使程序處于等待狀態(tài),有了gevent為我們自動(dòng)切換協(xié)程,就保證總有g(shù)reenlet在運(yùn)行,而不是等待IO。
由于切換是在IO操作時(shí)自動(dòng)完成,所以gevent需要修改Python自帶的一些標(biāo)準(zhǔn)庫(kù),這一過(guò)程在啟動(dòng)時(shí)通過(guò)monkey patch完成:
from gevent import monkey; monkey.patch_socket() import gevent import timedef f(n):for i in range(n):gevent.sleep(1)print gevent.getcurrent(), ig1 = gevent.spawn(f, 5) g2 = gevent.spawn(f, 5) g3 = gevent.spawn(f, 5) g1.join() g2.join() g3.join()運(yùn)行結(jié)果:
<Greenlet at 0x2134d91b9d0: f(5)> 0 <Greenlet at 0x2134dd91e10: f(5)> 1 <Greenlet at 0x2134dd91bf0: f(5)> 2 <Greenlet at 0x2134d91b9d0: f(5)> 3 <Greenlet at 0x2134dd91e10: f(5)> 4 <Greenlet at 0x2134dd91bf0: f(5)> 0 <Greenlet at 0x2134d91b9d0: f(5)> 1 <Greenlet at 0x2134dd91e10: f(5)> 2 <Greenlet at 0x2134dd91bf0: f(5)> 3 <Greenlet at 0x2134d91b9d0: f(5)> 4 <Greenlet at 0x2134dd91e10: f(5)> 0 <Greenlet at 0x2134dd91bf0: f(5)> 1 <Greenlet at 0x2134d91b9d0: f(5)> 2 <Greenlet at 0x2134dd91e10: f(5)> 3 <Greenlet at 0x2134dd91bf0: f(5)> 4可以看到,3個(gè)greenlet是依次運(yùn)行而不是交替運(yùn)行。
要讓greenlet交替運(yùn)行,可以通過(guò)gevent.sleep()交出控制權(quán):
def f(n):for i in range(n):print gevent.getcurrent(), igevent.sleep(0)執(zhí)行結(jié)果:
<Greenlet at 0x10cd58550: f(5)> 0 <Greenlet at 0x10cd58910: f(5)> 0 <Greenlet at 0x10cd584b0: f(5)> 0 <Greenlet at 0x10cd58550: f(5)> 1 <Greenlet at 0x10cd584b0: f(5)> 1 <Greenlet at 0x10cd58910: f(5)> 1 <Greenlet at 0x10cd58550: f(5)> 2 <Greenlet at 0x10cd58910: f(5)> 2 <Greenlet at 0x10cd584b0: f(5)> 2 <Greenlet at 0x10cd58550: f(5)> 3 <Greenlet at 0x10cd584b0: f(5)> 3 <Greenlet at 0x10cd58910: f(5)> 3 <Greenlet at 0x10cd58550: f(5)> 4 <Greenlet at 0x10cd58910: f(5)> 4 <Greenlet at 0x10cd584b0: f(5)> 43個(gè)greenlet交替運(yùn)行,
把循環(huán)次數(shù)改為500000,讓它們的運(yùn)行時(shí)間長(zhǎng)一點(diǎn),然后在操作系統(tǒng)的進(jìn)程管理器中看,線程數(shù)只有1個(gè)。
當(dāng)然,實(shí)際代碼里,我們不會(huì)用gevent.sleep()去切換協(xié)程,而是在執(zhí)行到IO操作時(shí),gevent自動(dòng)切換,代碼如下:
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/'), ])運(yùn)行結(jié)果:
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/.從結(jié)果看,3個(gè)網(wǎng)絡(luò)操作是并發(fā)執(zhí)行的,而且結(jié)束順序不同,但只有一個(gè)線程。
?
1 關(guān)于greenlet
greelet指的是使用一個(gè)任務(wù)調(diào)度器和一些生成器或者協(xié)程實(shí)現(xiàn)協(xié)作式用戶(hù)空間多線程的一種偽并發(fā)機(jī)制,即所謂的微線程。
greelet機(jī)制的主要思想是:生成器函數(shù)或者協(xié)程函數(shù)中的yield語(yǔ)句掛起函數(shù)的執(zhí)行,直到稍后使用next()或send()操作進(jìn)行恢復(fù)為止。可以使用一個(gè)調(diào)度器循環(huán)在一組生成器函數(shù)之間協(xié)作多個(gè)任務(wù)。
網(wǎng)絡(luò)框架的幾種基本的網(wǎng)絡(luò)I/O模型:
阻塞式單線程:這是最基本的I/O模型,只有在處理完一個(gè)請(qǐng)求之后才會(huì)處理下一個(gè)請(qǐng)求。它的缺點(diǎn)是效能差,如果有請(qǐng)求阻塞住,會(huì)讓服務(wù)無(wú)法繼續(xù)接受請(qǐng)求。但是這種模型編寫(xiě)代碼相對(duì)簡(jiǎn)單,在應(yīng)對(duì)訪問(wèn)量不大的情況時(shí)是非常適合的。
阻塞式多線程:針對(duì)于單線程接受請(qǐng)求量有限的缺點(diǎn),一個(gè)很自然的想法就是給每一個(gè)請(qǐng)求開(kāi)一個(gè)線程去處理。這樣做的好處是能夠接受更多的請(qǐng)求,缺點(diǎn)是在線程產(chǎn)生到一定數(shù)量之后,進(jìn)程之間需要大量進(jìn)行切換上下文的操作,會(huì)占用CPU大量的時(shí)間,不過(guò)這樣處理的話編寫(xiě)代碼的難道稍高于單進(jìn)程的情況。
非阻塞式事件驅(qū)動(dòng):為了解決多線程的問(wèn)題,有一種做法是利用一個(gè)循環(huán)來(lái)檢查是否有網(wǎng)絡(luò)IO的事件發(fā)生,以便決定如何來(lái)進(jìn)行處理(reactor設(shè)計(jì)模式)。這樣的做的好處是進(jìn)一步降低了CPU的資源消耗。缺點(diǎn)是這樣做會(huì)讓程序難以編寫(xiě),因?yàn)檎?qǐng)求接受后的處理過(guò)程由reactor來(lái)決定,使得程序的執(zhí)行流程難以把握。當(dāng)接受到一個(gè)請(qǐng)求后如果涉及到阻塞的操作,這個(gè)請(qǐng)求的處理就會(huì)停下來(lái)去接受另一個(gè)請(qǐng)求,程序執(zhí)行的流程不會(huì)像線性程序那樣直觀。twisted框架就是應(yīng)用這種IO模型的典型例子。
非阻塞式Coroutine(協(xié)程):這個(gè)模式是為了解決事件驅(qū)動(dòng)模型執(zhí)行流程不直觀的問(wèn)題,它在本質(zhì)上也是事件驅(qū)動(dòng)的,加入了Coroutine的概念。
2 與線程/進(jìn)程的區(qū)別
線程是搶占式的調(diào)度,多個(gè)線程并行執(zhí)行,搶占共同的系統(tǒng)資源;而微線程是協(xié)同式的調(diào)度。
其實(shí)greenlet不是一種真正的并發(fā)機(jī)制,而是在同一線程內(nèi),在不同函數(shù)的執(zhí)行代碼塊之間切換,實(shí)施“你運(yùn)行一會(huì)、我運(yùn)行一會(huì)”,并且在進(jìn)行切換時(shí)必須指定何時(shí)切換以及切換到哪。greenlet的接口是比較簡(jiǎn)單易用的,但是使用greenlet時(shí)的思考方式與其他并發(fā)方案存在一定區(qū)別:
線程/進(jìn)程模型在大邏輯上通常從并發(fā)角度開(kāi)始考慮,把能夠并行處理的并且值得并行處理的任務(wù)分離出來(lái),在不同的線程/進(jìn)程下運(yùn)行,然后考慮分離過(guò)程可能造成哪些互斥、沖突問(wèn)題,將互斥的資源加鎖保護(hù)來(lái)保證并發(fā)處理的正確性。
greenlet則是要求從避免阻塞的角度來(lái)進(jìn)行開(kāi)發(fā),當(dāng)出現(xiàn)阻塞時(shí),就顯式切換到另一段沒(méi)有被阻塞的代碼段執(zhí)行,直到原先的阻塞狀況消失以后,再人工切換回原來(lái)的代碼段繼續(xù)處理。因此,greenlet本質(zhì)是一種合理安排了的 串行 。
greenlet本質(zhì)是串行,因此在沒(méi)有進(jìn)行顯式切換時(shí),代碼的其他部分是無(wú)法被執(zhí)行到的,如果要避免代碼長(zhǎng)時(shí)間占用運(yùn)算資源造成程序假死,那么還是要將greenlet與線程/進(jìn)程機(jī)制結(jié)合使用(每個(gè)線程、進(jìn)程下都可以建立多個(gè)greenlet,但是跨線程/進(jìn)程時(shí)greenlet之間無(wú)法切換或通訊)。
3 使用
一個(gè) “greenlet” 是一個(gè)很小的獨(dú)立微線程。可以把它想像成一個(gè)堆棧幀,棧底是初始調(diào)用,而棧頂是當(dāng)前greenlet的暫停位置。你使用greenlet創(chuàng)建一堆這樣的堆棧,然后在他們之間跳轉(zhuǎn)執(zhí)行。跳轉(zhuǎn)不是絕對(duì)的:一個(gè)greenlet必須選擇跳轉(zhuǎn)到選擇好的另一個(gè)greenlet,這會(huì)讓前一個(gè)掛起,而后一個(gè)恢復(fù)。兩 個(gè)greenlet之間的跳轉(zhuǎn)稱(chēng)為 切換(switch) 。
當(dāng)你創(chuàng)建一個(gè)greenlet,它得到一個(gè)初始化過(guò)的空堆棧;當(dāng)你第一次切換到它,他會(huì)啟動(dòng)指定的函數(shù),然后切換跳出greenlet。當(dāng)最終棧底 函數(shù)結(jié)束時(shí),greenlet的堆棧又編程空的了,而greenlet也就死掉了。greenlet也會(huì)因?yàn)橐粋€(gè)未捕捉的異常死掉。
示例:來(lái)自官方文檔示例
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()最后一行跳轉(zhuǎn)到 test1() ,它打印12,然后跳轉(zhuǎn)到 test2() ,打印56,然后跳轉(zhuǎn)回 test1() ,打印34,然后 test1() 就結(jié)束,gr1死掉。這時(shí)執(zhí)行會(huì)回到原來(lái)的 gr1.switch() 調(diào)用。注意,78是不會(huì)被打印的,因?yàn)間r1已死,不會(huì)再切換。
4 基于greenlet的框架
4.1 eventlet
eventlet 是基于 greenlet 實(shí)現(xiàn)的面向網(wǎng)絡(luò)應(yīng)用的并發(fā)處理框架,提供“線程”池、隊(duì)列等與其他 Python 線程、進(jìn)程模型非常相似的 api,并且提供了對(duì) Python 發(fā)行版自帶庫(kù)及其他模塊的超輕量并發(fā)適應(yīng)性調(diào)整方法,比直接使用 greenlet 要方便得多。
其基本原理是調(diào)整 Python 的 socket 調(diào)用,當(dāng)發(fā)生阻塞時(shí)則切換到其他 greenlet 執(zhí)行,這樣來(lái)保證資源的有效利用。需要注意的是:
eventlet 提供的函數(shù)只能對(duì) Python 代碼中的 socket 調(diào)用進(jìn)行處理,而不能對(duì)模塊的 C 語(yǔ)言部分的 socket 調(diào)用進(jìn)行修改。對(duì)后者這類(lèi)模塊,仍然需要把調(diào)用模塊的代碼封裝在 Python 標(biāo)準(zhǔn)線程調(diào)用中,之后利用 eventlet 提供的適配器實(shí)現(xiàn) eventlet 與標(biāo)準(zhǔn)線程之間的協(xié)作。
雖然 eventlet 把 api 封裝成了非常類(lèi)似標(biāo)準(zhǔn)線程庫(kù)的形式,但兩者的實(shí)際并發(fā)執(zhí)行流程仍然有明顯區(qū)別。在沒(méi)有出現(xiàn) I/O 阻塞時(shí),除非顯式聲明,否則當(dāng)前正在執(zhí)行的 eventlet 永遠(yuǎn)不會(huì)把 cpu 交給其他的 eventlet,而標(biāo)準(zhǔn)線程則是無(wú)論是否出現(xiàn)阻塞,總是由所有線程一起爭(zhēng)奪運(yùn)行資源。所有 eventlet 對(duì) I/O 阻塞無(wú)關(guān)的大運(yùn)算量耗時(shí)操作基本沒(méi)有什么幫助。
4.2 gevent
4.2.1 gevent是一個(gè)基于協(xié)程(coroutine)的Python網(wǎng)絡(luò)函數(shù)庫(kù),通過(guò)使用greenlet提供了一個(gè)在libev事件循環(huán)頂部的高級(jí)別并發(fā)API。
主要特性有以下幾點(diǎn):
基于libev的快速事件循環(huán),Linux上面的是epoll機(jī)制
基于greenlet的輕量級(jí)執(zhí)行單元
API復(fù)用了Python標(biāo)準(zhǔn)庫(kù)里的內(nèi)容
支持SSL的協(xié)作式sockets
可通過(guò)線程池或c-ares實(shí)現(xiàn)DNS查詢(xún)
通過(guò)monkey patching功能來(lái)使得第三方模塊變成協(xié)作式
ps:
1、關(guān)于Linux的epoll機(jī)制:
epoll是Linux內(nèi)核為處理大批量文件描述符而作了改進(jìn)的poll,是Linux下多路復(fù)用IO接口select/poll的增強(qiáng)版本,它能顯著提高程序在大量并發(fā)連接中只有少量活躍的情況下的系統(tǒng)CPU利用率。epoll的優(yōu)點(diǎn):
支持一個(gè)進(jìn)程打開(kāi)大數(shù)目的socket描述符。select的一個(gè)進(jìn)程所打開(kāi)的FD由FD_SETSIZE的設(shè)置來(lái)限定,而epoll沒(méi)有這個(gè)限制,它所支持的FD上限是最大可打開(kāi)文件的數(shù)目,遠(yuǎn)大于2048。
IO效率不隨FD數(shù)目增加而線性下降:由于epoll只會(huì)對(duì)“活躍”的socket進(jìn)行操作,于是,只有”活躍”的socket才會(huì)主動(dòng)去調(diào)用 callback函數(shù),其他idle狀態(tài)的socket則不會(huì)。
使用mmap加速內(nèi)核與用戶(hù)空間的消息傳遞。epoll是通過(guò)內(nèi)核于用戶(hù)空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。
內(nèi)核微調(diào)。
2、libev機(jī)制
提供了指定文件描述符事件發(fā)生時(shí)調(diào)用回調(diào)函數(shù)的機(jī)制。libev是一個(gè)事件循環(huán)器:向libev注冊(cè)感興趣的事件,比如socket可讀事件,libev會(huì)對(duì)所注冊(cè)的事件的源進(jìn)行管理,并在事件發(fā)生時(shí)觸發(fā)相應(yīng)的程序。
4.2.2 官方文檔中的示例:
import geventfrom gevent import socketurls = [‘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,然后通過(guò)gevent.joinall將jobs加入到微線程執(zhí)行隊(duì)列中等待其完成,設(shè)置超時(shí)為2秒。執(zhí)行后的結(jié)果通過(guò)檢查gevent.Greenlet.value值來(lái)收集。gevent.socket.gethostbyname()函數(shù)與標(biāo)準(zhǔn)的socket.gethotbyname()有相同的接口,但它不會(huì)阻塞整個(gè)解釋器,因此會(huì)使得其他的greenlets跟隨著無(wú)阻的請(qǐng)求而執(zhí)行。
4.2.3 Monket patching
Python的運(yùn)行環(huán)境允許我們?cè)谶\(yùn)行時(shí)修改大部分的對(duì)象,包括模塊、類(lèi)甚至函數(shù)。雖然這樣做會(huì)產(chǎn)生“隱式的副作用”,而且出現(xiàn)問(wèn)題很難調(diào)試,但在需要修改Python本身的基礎(chǔ)行為時(shí),Monkey patching就派上用場(chǎng)了。Monkey patching能夠使得gevent修改標(biāo)準(zhǔn)庫(kù)里面大部分的阻塞式系統(tǒng)調(diào)用,包括socket,ssl,threading和select等模塊,而變成協(xié)作式運(yùn)行。
from gevent import monkey ;monkey . patch_socket ()import urllib2通過(guò)monkey.patch_socket()方法,urllib2模塊可以使用在多微線程環(huán)境,達(dá)到與gevent共同工作的目的。
4.2.4 事件循環(huán)
不像其他網(wǎng)絡(luò)庫(kù),gevent和eventlet類(lèi)似, 在一個(gè)greenlet中隱式開(kāi)始事件循環(huán)。沒(méi)有必須調(diào)用run()或dispatch()的反應(yīng)器(reactor),在twisted中是有 reactor的。當(dāng)gevent的API函數(shù)想阻塞時(shí),它獲得Hub實(shí)例(執(zhí)行時(shí)間循環(huán)的greenlet),并切換過(guò)去。如果沒(méi)有集線器實(shí)例則會(huì)動(dòng)態(tài) 創(chuàng)建。
libev提供的事件循環(huán)默認(rèn)使用系統(tǒng)最快輪詢(xún)機(jī)制,設(shè)置LIBEV_FLAGS環(huán)境變量可指定輪詢(xún)機(jī)制。LIBEV_FLAGS=1為select, LIBEV_FLAGS = 2為poll, LIBEV_FLAGS = 4為epoll,LIBEV_FLAGS = 8為kqueue。
Libev的API位于gevent.core下。注意libev API的回調(diào)在Hub的greenlet運(yùn)行,因此使用同步greenlet的API。可以使用spawn()和Event.set()等異步API。
總結(jié)
以上是生活随笔為你收集整理的Python 协程gevent的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C# 保留两位小数
- 下一篇: Python之eval函数实例解释