參考資料
Python腳本的執(zhí)行效率一直來說并不是很高灭袁,特別是Python下的多線程機制渠概,長久以來一直被人們詬病税肪。很多人都在思考如何讓Python執(zhí)行的更快一些剖煌,其中典型的方式包括:
- 將復雜的代碼轉(zhuǎn)由C語言完成
- 多進程并發(fā)執(zhí)行
- 多線程完成IO操作
然后裆装,人們討論的更多的則是Gevent的協(xié)程機制晕粪。在理解Gevent之前颠蕴,我們需要弄明白幾個基本的概念:進程(Process)泡挺、線程(Thread)辈讶、協(xié)程(Coroutine)、計算機IO方式等娄猫。
協(xié)程
進程和線程都是操作系統(tǒng)中的模型贱除,操作系統(tǒng)通過進程和線程這兩種模型來執(zhí)行程序。進程是操作系統(tǒng)分配資源(如CPU媳溺、內(nèi)存等)和調(diào)度的基本單位月幌,可以將其看作是包含系統(tǒng)分配的資源和執(zhí)行流兩部分,通過進程模型悬蔽,操作系統(tǒng)可以靈活地管理程序的執(zhí)行扯躺。線程是執(zhí)行流,一般而言一個進程只包含一個執(zhí)行流蝎困,也就是說一個進程只包含一個線程录语,通過線程模型,一個進程可以擁有多個執(zhí)行流难衰,進而提供程序的處理能力钦无。
協(xié)程coroutine
其實是corporate routine
的縮寫,直譯為協(xié)同的例程盖袭,簡稱為協(xié)程失暂。在Linux中線程是輕量級的進程,因此也將協(xié)程coroutine
稱為輕量級的線程鳄虱,又稱為微線程弟塞。協(xié)程簡單的理解就是程序的執(zhí)行流,在暫停和再次執(zhí)行的過程中可以記錄當前的狀態(tài)拙已,在暫停后需要再次執(zhí)行時可以從暫停前的狀態(tài)繼續(xù)執(zhí)行决记。協(xié)程暫停執(zhí)行時,可以調(diào)度到另一個協(xié)程執(zhí)行倍踪,這兩個協(xié)程之間的關(guān)系是對等的系宫。
協(xié)程和生成器generator
的概念很像,生成器也可以保存當前執(zhí)行狀態(tài)并再次運行時恢復之前的狀態(tài)建车,不過區(qū)別在于協(xié)程暫停時可以調(diào)度到另一個協(xié)程執(zhí)行扩借,而生成器暫停時會由它的調(diào)用者繼續(xù)執(zhí)行。
協(xié)程的調(diào)度由使用者所決定缤至,而不像進程和線程那樣由操作系統(tǒng)進行調(diào)度潮罪,Gevent中的協(xié)程在暫停時會執(zhí)行一個稱為Hub的協(xié)程,由Hub選擇處于等待執(zhí)行狀態(tài)的協(xié)程繼續(xù)執(zhí)行流。
線程是搶占式的調(diào)度嫉到,多個線程并行執(zhí)行時搶占共同的系統(tǒng)資源沃暗,而協(xié)程則是協(xié)同式的調(diào)度。其實Greenlet并非一種真正的并發(fā)機制何恶,而是在同一線程內(nèi)的不同函數(shù)的執(zhí)行代碼塊之間進行切換孽锥,實施“你運行一會兒,我運行一會兒”细层,在進行切換時必須制定何時切換以及切換到哪兒忱叭。
進程與協(xié)程
進程與協(xié)程有什么異同點呢?
進程與協(xié)程都可以看作是一種執(zhí)行流今艺,執(zhí)行流可以掛起,后續(xù)可以在掛起的位置恢復執(zhí)行爵卒。
例如:在Linux的Shell中執(zhí)行Hello程序
開始時Shell進程在運行并等待命令行的輸入虚缎,當執(zhí)行Hello程序時,Shell通過系統(tǒng)調(diào)用來執(zhí)行請求钓株,此時系統(tǒng)調(diào)用會將控制權(quán)傳遞給操作系統(tǒng)实牡,操作系統(tǒng)保存Shell進程的上下文并創(chuàng)建Hello進程以及上下文并將控制權(quán)交給Hello進程。當Hello進程終止后操作系統(tǒng)恢復Shell進程的上下文轴合,并將控制權(quán)傳回給Shell進程创坞,Shell進程繼續(xù)等待下一個命令的輸入。
當掛起一個執(zhí)行流時受葛,此時需要保存兩樣東西题涨,其一是棧,其實在切換前局部變量以及函數(shù)的調(diào)用都需要保存否則將無法恢復总滩,其二是寄存器狀態(tài)纲堵,寄存器狀態(tài)用于當執(zhí)行流恢復后需要執(zhí)行什么。寄存器和棧的結(jié)合可以理解為上下文闰渔,上下文切換的理解是CPU看上去像是在并行執(zhí)行多個進程席函,這其實是通過CPU在進程間切換來實現(xiàn)的,操作系統(tǒng)實現(xiàn)這種交錯執(zhí)行的機制稱為上下文切換冈涧。操作系統(tǒng)保存跟蹤進程運行所需的所有狀態(tài)信息茂附,這種狀態(tài)就是上下文。在任意時刻操作系統(tǒng)只能執(zhí)行一個進程代碼督弓,當操作系統(tǒng)決定將控制權(quán)從當前進程轉(zhuǎn)移到某個進程時营曼,就會進行上下文切換,也就是保存當前進程的上下文咽筋,并恢復新進程的上下文溶推。然后將控制權(quán)傳遞給新進程,新進程從從它上次停止的地方開始。
進程與協(xié)程的不同點在于
- 執(zhí)行流的調(diào)用者不同蒜危,進程是內(nèi)核調(diào)度虱痕,而協(xié)程是用戶態(tài)調(diào)度,也就是說進程的上下文是在內(nèi)核態(tài)中保存并恢復的辐赞,而協(xié)程是在用戶態(tài)保存恢復的部翘,很顯然用戶態(tài)的代價要跟低。
- 進程會被強占响委,而協(xié)程不會新思。也就是說協(xié)程如果不主動讓出CPU,那么其他協(xié)程就沒有執(zhí)行的機會赘风。
- 對內(nèi)存的占用不同夹囚,協(xié)程只需要4KB的棧空間就足夠了邀窃,而進程占用的內(nèi)存要大的多荸哟。
- 從操作系統(tǒng)角度講瞬捕,多協(xié)程的程序是單進程單協(xié)程的。
線程與協(xié)程
線程與協(xié)程有什么異同點呢肪虎?
協(xié)程也被稱為微線程,線程之間上下文切換的成本相對于協(xié)程而言是比較高的扇救,尤其是開啟的線程較多的時候。而協(xié)程的切換成本則比較低仅讽。另外,同樣線程的切換更多的是依靠操作系統(tǒng)來控制钾挟,而協(xié)程的執(zhí)行則是由用戶自己來控制洁灵。
計算機IO方式
根據(jù)計算機組成原理,計算機中IO的控制方式包括程序查詢方式掺出、程序中斷方式徽千、DMA方式、通道方式汤锨。目前計算機采用DMA和通道方式進行IO控制双抽,這樣在進行IO操作時,CPU可以盡量不參與到這個過程中而去執(zhí)行其它的操作闲礼。由于IO操作一般都比較耗時牍汹,采用DMA和通道方式可以將CPU從IO過程中解放出來铐维,從而提高系統(tǒng)的效率。
在程序運行過程中一把會遇到兩種類型的IO慎菲,即當前機器的磁盤IO和網(wǎng)絡IO嫁蛇,這兩種IO操作一般會阻塞程序的執(zhí)行,浪費CPU時間露该,因為此時程序分配到了時間片睬棚,在該時間片內(nèi)程序是獨占CPU資源,由于IO被阻塞CPU沒有被其他程序享有進而被浪費解幼。因此在編寫高性能程序時抑党,IO是需要重點關(guān)注的。
目前可以通過如下途徑解決因IO帶來的效率問題:
- 減少IO次數(shù)撵摆,也就是優(yōu)化程序的結(jié)構(gòu)底靠,將需要讀寫的數(shù)據(jù)匯集在一起進行一次性讀寫。
- 提高硬件的IO速度特铝,比如使用SSD磁盤苛骨。
- IO時不阻塞當前執(zhí)行流,由DMA控制器或通道負責IO操作苟呐,CPU繼續(xù)執(zhí)行程序的其他部分或執(zhí)行其他程序膛檀。
大多涉及到IO的高性能庫一般都是通過第三種途徑解決IO的性能瓶頸修噪,例如Tornado的異步操作,而Gevent正是基于Greenlet的協(xié)程巡李。Gevent實現(xiàn)了Python標準庫中一些阻塞庫的非阻塞版本笆呆,如socket
赠幕、os
榕堰、select
等逆屡,可以使用這些非阻塞的庫替代Python中阻塞的庫魏蔗。
網(wǎng)絡IO模型
- 阻塞式單線程
最基本的IO模型莺治,只有在處理完畢一個請求后才能處理下一個請求,缺點是效能差床佳,如果有請求阻塞住夕土,會讓服務無法繼續(xù)接受請求怨绣,但這種模型編寫代碼相對比較簡單篮撑,在應對訪問量不大的情況下非常適用赢笨。
- 阻塞式多線程
針對于單線程接受請求數(shù)量有限的缺點茧妒,一個很自然的想法是給每個請求開一個線程去處理桐筏,這樣做的好處是能夠接受更多的請求梅忌。缺點是當線程產(chǎn)生到一定數(shù)量之后牧氮,進程之間需要大量上下文切換的操作踱葛,此時會占用CPU大量的時間剖毯,不過這樣處理的話編寫代碼的難度稍高于單進程的情況逊谋。
- 非阻塞式事件驅(qū)動
為了解決多線程的問題胶滋,有一種做法是利用循環(huán)來檢查是否有網(wǎng)絡IO事件的發(fā)生,以便決定如何來進行處理俭令,比如Reactor設(shè)計模式抄腔。這種做法的好處是進一步降低了CPU的資源消耗赫蛇,缺點是這樣做會讓程序難以編寫悟耘,因為請求接受后的處理過程由Reactor來決定暂幼,使得程序的執(zhí)行流程難以把握旺嬉。當接收到一個請求后如果涉及到阻塞的操作鹰服,這個請求的處理過程會停下來去接受另一個請求,程序執(zhí)行的流程不會像線性程序那樣直觀亲善,比如Twisted框架就是采用此種模型的典型案例蛹头。
- 非阻塞式協(xié)程
非阻塞式協(xié)程是為了解決事件驅(qū)動模型執(zhí)行流程不直觀的問題渣蜗,在本質(zhì)上也是事件驅(qū)動的耕拷,但加入了協(xié)程的概念骚烧。
Gevent
Gevent是一種基于協(xié)程的Python網(wǎng)絡庫,使用Greenlet提供并封裝了libevent
事件循環(huán)的高層同步API既峡,使開發(fā)者在不改變編程習慣的同時运敢,以同步的方式編寫異步IO代碼传惠。簡單來說档痪,Gevent是基于libev
和Greenlet的一個異步的Python框架腐螟。
libev
是一個高性能的事件循環(huán)event loop
實現(xiàn)衬廷。事件循環(huán)(IO多路復用)是解決阻塞問題實現(xiàn)并發(fā)的一種方式汽绢。事件循環(huán)event loop
會捕獲并處理IO事件的變化宁昭,當遇到阻塞時就會跳出积仗,當阻塞結(jié)束時則會繼續(xù)寂曹。這一點依賴于操作系統(tǒng)底層的select
函數(shù)及其升級版poll
和epoll
隆圆。而Greenlet則是一個Python的協(xié)程管理和切換的模塊渺氧,通過Greenlet可以顯式地在不同的任務之間進行切換侣背。
Libev
Gevent的基本原理來自于libevent
&libev
,熟悉C語言的同學應該對這個lib
不陌生业筏。本質(zhì)上libevent
或者說libev
都是一種事件驅(qū)動模型鸟赫。這種模型對于提高CPU的運行效率台谢,增強用戶的并發(fā)訪問非常有效朋沮。但因為其本身是一種事件機制缀壤,所以編寫起來有些繞筋夏,并不是很直觀图呢。因此為了修正這個問題蛤织,有人引入了用戶態(tài)上下文切換機制指蚜,也就是說,如果代碼中引入了帶IO阻塞的代碼時,lib
本身會自動完成上下文的切換柱宦,全程用戶都是沒有察覺的,這又是Gevent的由來播瞳。
Libev是高性能事件循環(huán)模型的網(wǎng)絡庫忧侧,包含大量新特性,是繼libevent
之后的一套全新的網(wǎng)絡庫松逊。libev
追求的目標是速度更快经宏、bug更少、特性更多徊都、體積更小暇矫。和libevent
類似杀餐,可以作為其替代者朱巨,提供更高的性能且無需復雜的配置琼讽。
libev
機制提供了對指定文件描述符發(fā)生時調(diào)用回調(diào)函數(shù)的機制钻蹬,libev
是一個事件循環(huán)器问欠,向libev
注冊感興趣的事件顺献,比如Socket可讀事件注整,libev
會對所注冊的事件的源進行管理肿轨,并在事件發(fā)生時出發(fā)相應的程序椒袍。
Yield
Python對協(xié)程的支持是非常有限的曙蒸,使用生成器generator
中的yield
可以一定程序上實現(xiàn)協(xié)程纽窟。比如傳統(tǒng)的生產(chǎn)者-消費者模型臂港,即一個線程寫消息一個線程讀消息,通過鎖機制控制隊列和等待佑力,但一不小心就可能出現(xiàn)死鎖打颤。如果改用協(xié)程,生產(chǎn)者生產(chǎn)消息后直接通過yield
跳轉(zhuǎn)到消費者并開始執(zhí)行响驴,等消費者執(zhí)行完畢后再切換回生產(chǎn)者繼續(xù)生產(chǎn)秽誊,這樣做效率極高。
$ vim test.py
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
import time
def consumer():
r = ""
while True:
n = yield r
if not n:
return
print("consumer %s"%n)
r = "200 OK"
def producer(c):
c.__next__()
n = 0
while n < 3:
n = n + 1
print("producer %s"%n)
r = c.send(n)
print("producer return %s\n"%r)
c.close()
if __name__ == "__main__":
c = consumer()
producer(c)
$ python3 test.py
producer 1
consumer 1
producer return 200 OK
producer 2
consumer 2
producer return 200 OK
producer 3
consumer 3
producer return 200 OK
代碼分析:首先調(diào)用c.__next__()
啟動生成器日熬,一旦生產(chǎn)出東西棍厌,則通過c.send(n)
切換到消費者consumer
來執(zhí)行,消費者consumer
通過yield
獲取到消息后處理竖席,然后通過yield
將結(jié)果傳回耘纱。生產(chǎn)者producer
獲取到消費者consumer
處理的結(jié)果后繼續(xù)生產(chǎn)下一條消息。整個過程無鎖毕荐,由一個線程執(zhí)行束析,生產(chǎn)者和消費者協(xié)作完成任務,所以稱之為協(xié)程憎亚。
Python通過yield
提供了對協(xié)程的基本支持员寇,但并不完全蝶锋。而第三方的Gevent為Python提供了比較完善的協(xié)程支持别威,Gevent是第三方庫,可通過Greenlet實現(xiàn)協(xié)程。另外臀栈,Python中由于GIL的存在導致多線程一直不是很好用,相比之下,協(xié)程的優(yōu)勢就更加突出了。
Greenlet
Greenlet是指使用一個任務調(diào)度器和一些生成器或協(xié)程實現(xiàn)協(xié)作式用戶空間多線程的一種偽并發(fā)機制甩鳄,也就是所謂的微線程。Greenlet機制的主要思想是生成器函數(shù)或協(xié)程函數(shù)中的yield
語句掛起函數(shù)的執(zhí)行,直到稍后使用next()
或send()
操作進行恢復為止盆昙×锻牛可以使用一個調(diào)度器循環(huán)在一組生成器函數(shù)在將協(xié)作多個任務。
既然Gevent使用的是Greenlet,因此需要理解Greenlet的工作原理:每個協(xié)程都有一個parent
,最頂層的協(xié)程是man thread
或者是當前線程鲫咽,每個協(xié)程遇到IO時會見控制權(quán)交給最頂層的協(xié)程括丁,它會檢測到哪個協(xié)程的IO Event已經(jīng)完成并將控制權(quán)交給它。
Greenlet的基本思路是:當一個Greenlet遇到IO操作時己单,比如訪問網(wǎng)絡時會自動切換到其它的Greenlet廷痘,等到IO操作完成兄猩,再在適當?shù)臅r候切換回來繼續(xù)執(zhí)行。由于IO操作非常耗時,經(jīng)常會使程序處于等待狀態(tài)虐唠,有了Gevent自動切換協(xié)程,就保證總有Greenlet在運行撤蟆,而不是等待IO值依。由于切換是在IO操作時自動完成,所以Gevent需要修改Python自帶的標準庫碍沐,這一過程在啟動時通過monkey patch
猴子補丁來完成缔赠。
Swich
一個Greenlet是一個很小的獨立微線程置森,可以把它想象成一個堆棧幀崭捍,棧底是初始調(diào)用,棧頂是當前Greenlet的暫停位置宴猾,使用Greenlet創(chuàng)建一堆這樣的堆棧仇哆,然后在它們之間跳轉(zhuǎn)執(zhí)行延欠。跳轉(zhuǎn)并不是絕對的涧窒,因為一個Greenlet必須選擇跳轉(zhuǎn)到選擇好的另一個Greenlet戴已,這會讓前一個掛起,而后一個恢復羽资,兩個Greenlet之間的跳轉(zhuǎn)又被稱之為切換switch
。當創(chuàng)建一個Greenlet時它會得到一個初始化過的空堆棧遵班,當?shù)谝淮吻袚Q到它時會啟動指定的函數(shù)屠升,然后切換跳出Greenlet,當最終棧底函數(shù)結(jié)束時狭郑,Greenlet的堆棧又會變成空的腹暖,而Greenlet也就死掉了。當然翰萨,Greenlet也會因為一個未捕獲的異常而死掉脏答。
Monkey-patching
Monkey-patching猴子補丁這個叫法源自于Zope框架,大家在修正Zope的Bug時經(jīng)常會在程序后追加更新部分亩鬼,這些被稱作“雜牌軍補吨掣妗(guerillapatch)”,后來guerilla
逐漸寫成了gorllia
(猩猩)辛孵,再后來就寫成了monkey
(猴子)丛肮,所以猴子補丁的叫法就這么莫名其妙的得來了赡磅。之后在動態(tài)語言中魄缚,不改變源代碼而對功能進行追加和變更就統(tǒng)稱為“猴子補丁”焚廊。所以猴子補丁并不是Python中專有的冶匹,猴子補丁充分利用了動態(tài)語言的靈活性咆瘟,可以對現(xiàn)有語言API進行追加、替換袒餐、修改飞蛹,甚至性能優(yōu)化等。使用猴子補丁的方式Gevent能夠修改標準庫中大部分的阻塞式系統(tǒng)調(diào)用灸眼,包括socket
、ssl
焰宣、threading
、select
等模塊匕积,使其變?yōu)閰f(xié)作式運行盈罐。
Monkey-patching猴子補丁是將標準庫中大部分的阻塞式調(diào)用替換成非阻塞的方式榜跌,包括socket
、ssl
盅粪、threading
、select
湾揽、httplib
等。通過monkey.path_xxx()
函數(shù)來打補丁库物,根據(jù)Gevent文檔中的建議,應當將猴子補丁的代碼盡可能早的被調(diào)用戚揭,這樣可以避免一些奇怪的異常。
使用Gevent的性能要比傳統(tǒng)的線程高民晒,但不得不說的一個坑是如果使用Monkey-patching猴子補丁,Gevent將直接修改標準庫中大部分的阻塞式調(diào)用潜必,包括socket
靴姿、ssl
、threading
佛吓、select
等模塊,而變?yōu)閰f(xié)作式運行垂攘。但無法保證在復雜的生產(chǎn)環(huán)境中那些地方使用標準庫因補丁而出現(xiàn)的奇怪問題。另外是第三方庫的支持晒他,需要確保項目中使用到的其他網(wǎng)絡庫也必須使用純Python或明確支持Gevent吱型。
Gevent應該在什么場景中使用呢陨仅?
Gevent的優(yōu)勢在于可以通過同步的邏輯實現(xiàn)并發(fā)操作津滞,大大降低編寫并行或并發(fā)程序的難度灼伤。另外触徐,在一個進程中使用Gevent可以有效地避免對臨界資源的互斥訪問饺蔑。如果程序中涉及到較多的IO锌介,可以使用Gevent替代多線程來提高程序的效率,但是由于Gevent中的協(xié)程的調(diào)度是由使用者而非操作系統(tǒng)決定的,Gevent主要解決的問題是IO問題隆敢,通過提高IO-bound類型的程序的效率,另外由于是在一個進程中實現(xiàn)協(xié)程崔慧,而操作性i同是以進程為單位分配處理資源的(一個進程分配一個處理機)。因此惶室,Gevent并不適合對任務延遲有要求的場景温自,比如交互式程序中皇钞。也不適用于CPU-bound類型的任務和需要使用多處理機的場景(通過運行多個進程悼泌,每個進程內(nèi)實現(xiàn)協(xié)程來解決這個問題夹界。)馆里。
安裝使用
Ubuntu系統(tǒng)下可通過apt-get
安裝gevent
庫
$ sudo apt-get install python-gevent
如果使用的Python3的版本,安裝如下:
$ sudo apt-get install python3-gevent
也可以直接使用Python的包管理工具pip命令進行安裝鸠踪,不過需要注意版本與權(quán)限。
$ pip install gevent
入門案例:使用Gevent控制程序執(zhí)行順序
$ vim test.py
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
import gevent
from gevent import monkey
monkey.patch_socket()
def fn(n):
for i in range(n):
print(gevent.getcurrent(), i)
# 創(chuàng)建協(xié)程對象
greenlet1 = gevent.spawn(fn, 3)
greenlet2 = gevent.spawn(fn, 3)
# 等待greenlet1執(zhí)行結(jié)束
greenlet1.join()
greenlet2.join()
# 獲取fn的返回值
# print(greenlet1.value)
$ python3 test.py
<Greenlet at 0x7ff0b1f6e508: fn(3)> 0
<Greenlet at 0x7ff0b1f6e508: fn(3)> 1
<Greenlet at 0x7ff0b1f6e508: fn(3)> 2
<Greenlet at 0x7ff0b1f6ea60: fn(3)> 0
<Greenlet at 0x7ff0b1f6ea60: fn(3)> 1
<Greenlet at 0x7ff0b1f6ea60: fn(3)> 2
根據(jù)執(zhí)行結(jié)果可知:greenlet
是依次運行而不是交替運行的复斥,如果要讓greenlet
交替運行則需要通過gevent.sleep()
交出控制權(quán)。
green.spawn
會啟動所有協(xié)程目锭,協(xié)程都是運行在同一個線程之中的评汰,所以協(xié)程不能夠跨線程同步數(shù)據(jù)侣集。
$ vim test.py
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
import gevent
from gevent import monkey
monkey.patch_socket()
def fn(n):
for i in range(n):
print(gevent.getcurrent(), i)
gevent.sleep(0)
greenlet1 = gevent.spawn(fn, 3)
greenlet2 = gevent.spawn(fn, 3)
# 合并兩步操作
gevent.joinall([greenlet1, greenlet2])
$ python3 test.py
<Greenlet at 0x7f53b09a1508: fn(3)> 0
<Greenlet at 0x7f53b09a1a60: fn(3)> 0
<Greenlet at 0x7f53b09a1508: fn(3)> 1
<Greenlet at 0x7f53b09a1a60: fn(3)> 1
<Greenlet at 0x7f53b09a1508: fn(3)> 2
<Greenlet at 0x7f53b09a1a60: fn(3)> 2
程序的重要部分是將任務函數(shù)封裝到gevent.spawn
中键俱,初始化的Greenlet列表存放在數(shù)組線程中世分,此數(shù)組會被傳給gevent.joinall
函數(shù),gevent.joinall
函數(shù)會阻塞當前流程缀辩,并執(zhí)行所有給定的Greenlet,執(zhí)行流只會在所有的Greenlet執(zhí)行完畢后才會繼續(xù)向下執(zhí)行臀玄。
在Gevent中gevent.sleep()
模擬的是Gevent可以識別的IO阻塞瓢阴,若使用time.sleep()
或其他阻塞健无,Gevent是不能夠直接識別的荣恐,需要使用猴子補丁,注意猴子補丁必須放在被打補丁的前面,如time
叠穆、socket
模塊之前少漆。
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
import gevent
from gevent import monkey
monkey.patch_all()
import time
def fn(n):
for i in range(n):
print(gevent.getcurrent(), i)
time.sleep(1)
greenlet1 = gevent.spawn(fn, 3)
greenlet2 = gevent.spawn(fn, 3)
gevent.joinall([greenlet1, greenlet2])
案例:Greenlet模塊內(nèi)部使用了協(xié)程的概念硼被,在單線程內(nèi)需要手動調(diào)用switch
函數(shù)切換協(xié)程示损。
$ vim test.py
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
from greenlet import greenlet
def eat(name):
print("%s eat 1"%name)
g2.switch("egon")
print("%s eat 2"%name)
g2.switch()
def play(name):
print("%s play 1"%name)
g1.switch()
print("%s play 2"%name)
g1 = greenlet(eat)
g2 = greenlet(play)
g1.switch("egon")
$ python3 test.py
egon eat 1
egon play 1
egon eat 2
egon play 2
根據(jù)上述代碼可以發(fā)現(xiàn)嚷硫,協(xié)程一旦遇到IO操作就會自動切換到其它協(xié)程检访,使用yield
是無法實現(xiàn)的。
例如:
$ vim test.py
#! /usr/bin/env python3
# -*- coding:utf-8 -*-
import gevent
from gevent import socket
urls = [
"www.baidu.com",
"www.python.org",
"www.example.com"
]
jobs = [
gevent.spawn(socket.gethostbyname, url) for url in urls
]
gevent.joinall(jobs, timeout=2)
result = [
job.value for job in jobs
]
print(result)
$ python3 test.py
['112.80.248.75', '151.101.108.223', '93.184.216.34']
使用gevent.spawn
函數(shù)spawn
引發(fā)一些任務jobs
脆贵,再通過gevent.joinall
將所有任務jobs
加入到為協(xié)程執(zhí)行隊列中等待其完成,同時設(shè)置超時時間為2秒起暮。執(zhí)行后的結(jié)果通過檢查gevent.greenlet.value
值來收集。
gevent.socket.gethostbyname
函數(shù)與標準的socket.gethostbyname
有著相同的接口鞋怀,但不會阻塞整個解釋器双泪,因此會使其他Greenlet跟隨著無阻塞的請求而執(zhí)行密似。
猴子補丁
from gevent import monkey
patch_all
patch_all(
socket = True,
dns = True,
time = True,
select = True,
thread = True,
os = True,
ssl = True,
httplib = False,
subprocess = True,
sys = False,
aggressive = True,
Event = False,
builtins = True,
signal = True
)
Gevent的Monkey可以為socket焙矛、dns、time残腌、select、thread抛猫、os蟆盹、ssl闺金、httplib逾滥、subprocess、sys寨昙、aggressive、Event掀亩、builtins、signal模塊打上的補丁槽棍,打上補丁后他們就是非阻塞的了捉蚤。