Gevent是python的第三方庫,提供了比較完善的對(duì)協(xié)程的支持茬故。Python中GIL的存在盖灸,導(dǎo)致多線程一直不是很好用,相形之下磺芭,協(xié)程的優(yōu)勢(shì)就更加突出了赁炎。
Gevent的基本思想是:當(dāng)遇到IO操作時(shí),會(huì)自動(dòng)寫換到其他gevent钾腺,再在適當(dāng)?shù)臅r(shí)間切回來繼續(xù)執(zhí)行徙垫。這樣就減少了IO操作時(shí)的等待耗時(shí),從而能夠提高硬件資源的利用率放棒。
注:本文使用python版本2.7.12姻报, gevent版本1.2.2
1. greenlet/eventlet/gevent的關(guān)系
Greelent實(shí)現(xiàn)了一個(gè)比較易用(相比yeild)的協(xié)程切換的庫。但是greenlet沒有自己的調(diào)度過程间螟,所以一般不會(huì)直接使用吴旋。
Eventlet在Greenlet的基礎(chǔ)上實(shí)現(xiàn)了自己的GreenThread,實(shí)際上就是greenlet類的擴(kuò)展封裝厢破,而與Greenlet的不同是荣瑟,Eventlet實(shí)現(xiàn)了自己調(diào)度器稱為Hub,Hub類似于Tornado的IOLoop摩泪,是單實(shí)例的笆焰。在Hub中有一個(gè)event loop,根據(jù)不同的事件來切換到對(duì)應(yīng)的GreenThread加勤。同時(shí)Eventlet還實(shí)現(xiàn)了一系列的補(bǔ)丁來使Python標(biāo)準(zhǔn)庫中的socket等等module來支持GreenThread的切換仙辟。Eventlet的Hub可以被定制來實(shí)現(xiàn)自己調(diào)度過程。
Gevent基于libev和Greenlet鳄梅。不同于Eventlet的用python實(shí)現(xiàn)的hub調(diào)度叠国,Gevent通過Cython調(diào)用libev來實(shí)現(xiàn)一個(gè)高效的event loop調(diào)度循環(huán)。同時(shí)類似于Eventlet戴尸,Gevent也有自己的monkey_patch灭贷,在打了補(bǔ)丁后鳄逾,完全可以使用python線程的方式來無感知的使用協(xié)程炒嘲,減少了開發(fā)成本育勺。
2. gevent猴子補(bǔ)丁
猴子補(bǔ)丁monkey_patch,將標(biāo)準(zhǔn)庫中大部分的阻塞式調(diào)用替換成非阻塞的方式弊知,包括socket、ssl、threading合瓢、select、httplib等透典。通過monkey.patch_xxx()來打補(bǔ)丁晴楔。按照gevent文檔中的建議,應(yīng)該將猴子補(bǔ)丁的代碼盡可能早的被調(diào)用峭咒,這樣可以避免一些奇怪的異常税弃。
我是這樣理解的,gevent實(shí)現(xiàn)了協(xié)程的創(chuàng)建凑队、切換和調(diào)度则果,本身是同步的,而猴子補(bǔ)丁將gevent調(diào)用的阻塞庫變成非阻塞的漩氨,兩者配合實(shí)現(xiàn)了高性能的協(xié)程西壮。
3. gevent和popen
Gevent雖然提供了subprocess的支持,但是沒有提供對(duì)os.popen的支持才菠,os.system也是一樣茸时。也就是說,os.popen是阻塞的赋访。測(cè)試如下:
from gevent import monkey
monkey.patch_all()
import gevent
import os
def func(num):
print "start", num
os.popen("sleep 3")
# os.system("sleep 3")
print "end", num
g1 = gevent.spawn(func, 1)
g2 = gevent.spawn(func, 2)
g3 = gevent.spawn(func, 3)
g1.join()
g2.join()
g3.join()
說明一下可都,這里的join是用來阻塞主協(xié)程,用來做協(xié)程間同步用的蚓耽。和thread類似渠牲。
輸出結(jié)果
start 1
end 1
start 2
end 2
start 3
end 3
需要注意的是,不使用gevent時(shí)步悠, os.popen("sleep 3")本身是不阻塞的签杈,os.popen("sleep 3").read()才會(huì)阻塞。但是使用gevent時(shí)鼎兽,os.popen("sleep 3")也是會(huì)阻塞答姥。
但是使用subprocess就可以實(shí)現(xiàn)非阻塞式調(diào)用,subprocess.call和subprocess.Popen都是非阻塞的谚咬。測(cè)試如下:
from gevent import monkey
monkey.patch_all()
import gevent
import os
import subprocess
def func(num):
print "start", num
susubprocess.call(['sleep', '3'])
# sub = subprocess.Popen(['sleep 3'], shell=True)
# out, err = sub.communicate()
print "end", num
g1 = gevent.spawn(func, 1)
g2 = gevent.spawn(func, 2)
g3 = gevent.spawn(func, 3)
g1.join()
g2.join()
g3.join()
輸出結(jié)果
start 1
start 2
start 3
end 1
end 2
end 3
4. gevent和time
Monkey.patch_all會(huì)將time庫也變成非阻塞的鹦付,也就是說monkey.patch_all之后,time.sleep等同等于gevent.sleep择卦。測(cè)試如下:
from gevent import monkey
monkey.patch_all()
import gevent
import time
def func(num):
print "start", num
time.sleep(3)
print "start", num
g1 = gevent.spawn(func, 1)
g2 = gevent.spawn(func, 2)
g3 = gevent.spawn(func, 3)
g1.join()
g2.join()
g3.join()
輸出結(jié)果
start 1
start 2
start 3
end 1
end 2
end 3
當(dāng)然敲长,如果沒有monkey.patch_all或者monkey.patch_time的話郎嫁,time還是阻塞的。
可以查看patch_all的函數(shù)原型祈噪,就能知道打了哪些補(bǔ)对箢酢:
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)
可以看到httplib和Event默認(rèn)是關(guān)閉的,其他默認(rèn)都是開啟的辑鲤。
5. gevent和timeout
看到有文章說盔腔,gevent里使用timeout會(huì)失效,因?yàn)橐呀?jīng)是非阻塞的了遂填。
經(jīng)過驗(yàn)證铲觉,上面的說法是錯(cuò)誤的。無論使用urllib2吓坚,requests庫,timeout設(shè)置都有效灯荧。
from gevent import monkey
monkey.patch_all()
import gevent
import requests
import urllib2
def func(url):
# print "start", url
# urllib2.urlopen(url, timeout=3)
requests.get(url, timeout=3)
# print "end", url
g1 = gevent.spawn(func, "http://www.baidu.com")
g2 = gevent.spawn(func, "http://www.sina.com")
g3 = gevent.spawn(func, "http://www.google.com")
g1.join()
g2.join()
g3.join()
會(huì)有正常的超時(shí)報(bào)錯(cuò):
Traceback (most recent call last):
File "/usr/local/lib/python2.7/dist-packages/gevent/greenlet.py", line 536, in run
result = self._run(*self.args, **self.kwargs)
File "<stdin>", line 2, in func
File "/usr/lib/python2.7/urllib2.py", line 154, in urlopen
return opener.open(url, data, timeout)
File "/usr/lib/python2.7/urllib2.py", line 429, in open
response = self._open(req, data)
File "/usr/lib/python2.7/urllib2.py", line 447, in _open
'_open', req)
File "/usr/lib/python2.7/urllib2.py", line 407, in _call_chain
result = func(*args)
File "/usr/lib/python2.7/urllib2.py", line 1228, in http_open
return self.do_open(httplib.HTTPConnection, req)
File "/usr/lib/python2.7/urllib2.py", line 1198, in do_open
raise URLError(err)
URLError: <urlopen error [Errno 101] Network is unreachable>
Tue Jul 24 20:22:50 2018 <Greenlet at 0x7ff390f14af0: func('http://www.google.com')> failed with URLError
另外礁击,gevent里有個(gè)Timeout對(duì)象,可以很方便的實(shí)現(xiàn)非阻塞式的超時(shí)控制:
with gevent.Timeout(seconds, exception) as timeout:
pass # ... code block ...
如果不指定exception逗载,超時(shí)會(huì)raise timeout
6. gevent和數(shù)據(jù)庫操作
既然monkey.patch_all將socket變成非阻塞了哆窿,那么進(jìn)行數(shù)據(jù)庫操作請(qǐng)求,也會(huì)建立socket連接厉斟,自然也是非阻塞的挚躯。
比如redis:
from gevent import monkey
monkey.patch_all()
import gevent
import redis
r = redis.Redis(host="localhost",port=6379)
def func(key):
print "start", key
v = r.get(key)
print "end", key
g1 = gevent.spawn(func, "a")
g2 = gevent.spawn(func, "b")
g3 = gevent.spawn(func, "c")
g1.join()
g2.join()
g3.join()
結(jié)果
start a
start b
start c
end a
end b
end c
但是MySQL是阻塞的,因?yàn)椴粱啵琈ySQL是用C寫的码荔,patch的socket補(bǔ)丁,并不生效感挥。
from gevent import monkey
monkey.patch_all()
import gevent
import MySQLdb
def func(data):
print "start", data
conn = MySQLdb.connect(host="localhost",user="root",passwd="root",db="test")
cur = conn.cursor()
cur.execute("insert into test (data) values(%s)", (data,))
conn.commit()
print "end", data
g1 = gevent.spawn(func, "a")
g2 = gevent.spawn(func, "b")
g3 = gevent.spawn(func, "c")
g1.join()
g2.join()
g3.join()
輸出
start a
end a
start b
end b
start c
end c
6. gevent文件IO
注意:gevent里文件IO操作是不做切換的缩搅。
from gevent import monkey
monkey.patch_all()
import gevent
import os
def func(fn):
print "start", fn
with open(fn, "w") as f:
f.write("*"*100000000)
with open(fn) as f:
print len(f.read())
print "end", fn
g1 = gevent.spawn(func, "text1")
g2 = gevent.spawn(func, "text2")
g3 = gevent.spawn(func, "text3")
g1.join()
g2.join()
g3.join()
結(jié)果
start text1
100000000
end text1
start text2
100000000
end text2
start text3
100000000
end text3
6. gevent的結(jié)果和異常
Gevent運(yùn)行的結(jié)果和異常可以通過value和exception來獲取触幼。需要注意的是硼瓣,協(xié)程內(nèi)部運(yùn)行的異常,不會(huì)被拋出(會(huì)被打又们)從而影響到其他協(xié)程堂鲤。
print g1.value, g1.exception