协程是一中多任务实现方式,它不需要多个进程或线程就可以实现多任务。
gevent:
sudo apt-get install libevent-dev
sudo apt-get install python-dev
sudo easy-install gevent
- 基于协程的Python网络库
- API的概念和Python标准库一致(如事件,队列)。
- TCP/UDP/HTTP服务器
- 能够使用标准库和第三方模块创建标准的阻塞套接字
- 可以配合socket,ssl模块使用
- 子进程支持(通过gevent.subprocess)
- 基于greenlet的轻量级执行单元
- 线程池
greenlets是确定性的。给定相同的绿色配置和相同的输入集,它们总是产生相同的输出
gevent每次遇到io操作,需要耗时等待时,会自动跳到下一个协程继续执行。
- gevent的代码风格和线程非常相似,运行出来后的效果也非常相似。
原理:
程序的重要部分是将任务函数封装到gevent.spawn。初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,gevent.joinall会阻塞当前流程,并执行所有给定的greenlet,执行流程只会在所有greenlet执行完后才会继续向下走。
(2)gevent实现了python标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和select等模块,而将这些阻塞式调用变为协作式运行(参见猴子补丁部分)。
猴子补丁Monkey Patch:
(1)猴子补丁的由来
猴子补丁的这个叫法起源于Zope框架,大家在修正Zope的Bug的时候经常在程序后面追加更新部分,这些被称作是“杂牌军补丁(guerillapatch)”,后来guerilla就渐渐的写成了gorllia(猩猩),再后来就写了monkey(猴子),所以猴子补丁的叫法是这么莫名其妙的得来的。
后来在动态语言中,不改变源代码而对功能进行追加和变更,统称为“猴子补丁”。所以猴子补丁并不是Python中专有的。猴子补丁这种东西充分利用了动态语言的灵活性,可以对现有的语言Api进行追加,替换,修改Bug,甚至性能优化等等。
使用猴子补丁的方式,gevent能够修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。也就是通过猴子补丁的monkey.patch_xxx()来将python标准库中模块或函数改成gevent中的响应的具有协程的协作式对象。这样在不改变原有代码的情况下,将应用的阻塞式方法,变成协程式的。
(2)猴子补丁使用时的注意事项
猴子补丁的功能很强大,但是也带来了很多的风险,尤其是像gevent这种直接进行API替换的补丁,整个Python进程所使用的模块都会被替换,可能自己的代码能hold住,但是其它第三方库,有时候问题并不好排查,即使排查出来也是很棘手,所以,就像松本建议的那样,如果要使用猴子补丁,那么只是做功能追加,尽量避免大规模的API覆盖。
虽然猴子补丁仍然是邪恶的(evil),但在这种情况下它是“有用的邪恶(useful evil)”。
import gevent
# from gevent import monkey;monkey.patch_all() # 切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成
def A():
while 1:
print('-------A-------')
gevent.sleep(1) #用来模拟一个耗时操作,注意不是time模块中的sleep, 每当碰到耗时操作,会自动跳转至其他协程
def B():
while 1:
print('-------B-------')
gevent.sleep(0.5)
# gevent.joinall([gevent.spawn(fn)
g1 = gevent.spawn(A) # 创建一个协程
g2 = gevent.spawn(B)
g1.join() #等待协程执行结束
g2.join()
# 执行结果
# -------A-------
# -------B-------
# -------B-------
# -------A-------
# -------B-------
# -------B-------
# -------A-------
# -------B-------
# -------B-------
# ···
select()函数通常是对各种文件描述符进行轮询的阻塞调用。
from gevent import select
...
select.select([], [], [], 2)
gevent池
from gevent.pool import Pool
import time
def echo(i):
time.sleep(0.001)
return i
p = Pool(10)
run1 = [a for a in p.imap_unordered(echo, xrange(10))]
print(run1 == run2 == run3 == run4) # True
# 就像之前所提到的,greenlet具有确定性。在相同配置相同输入的情况下,它们总是会产生相同的输出
生成Greenlets(Spawning Greenlets)
程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn.
初始化的greenlet列表存放在数组中,此数组被传给gevent.joinall函数,后者阻塞当前流程,并执行所有给定的greenlet。执行流程只会在所有greenlet执行完后才会继续向下走。
一个更常见的应用场景,如异步地向服务器取数据,取数据操作的执行时间依赖于发起取数据请求时远端服务器的负载,各个请求的执行时间会有差别。
# 列表推导式生成任务队列
threads = [gevent.spawn(函数, 参数) for i in xrange(10)]
gevent.joinall(threads)
# -------------------------------------------------------
# for循环
threads = []
for i in range(1,10):
threads.append(gevent.spawn(fetch, i))
gevent.joinall(threads)
# ----------------------------------------------
# 单个运行
g2 = gevent.spawn(B)
g1.join()
import gevent
from gevent import Greenlet
def foo(message, n):
"""
每个线程都将传递message和n参数
"""
gevent.sleep(n)
print(message)
# 初始化运行命名函数的新Greenlet实例。
# foo
thread1 = Greenlet.spawn(foo, "Hello", 1)
# 用于创建和运行来自命名的新Greenlet的包装器。
# foo函数,带有传递的参数。
thread2 = gevent.spawn(foo, "I live!", 2)
# 匿名函数展示
thread3 = gevent.spawn(lambda x: (x+1), 2)
threads = [thread1, thread2, thread3]
# 阻塞,直到所有线程完成
gevent.joinall(threads)
# Hello
# I live!
除了使用基本的Greenlet类之外,您还可以子类化Greenlet类并覆盖_run方法。
import gevent
from gevent import Greenlet
class MyGreenlet(Greenlet):
def __init__(self, message, n):
Greenlet.__init__(self)
self.message = message
self.n = n
def _run(self):
print(self.message)
gevent.sleep(self.n)
g = MyGreenlet("Hi there!", 3)
g.start()
g.join()
Hi there!
Greenlet 状态(Greenlet State)
和其他代码段一样,Greenlets可能会以各种方式失败。greenlet可能无法抛出异常,造成不能停止或消耗太多的系统资源。
greenlet的内部状态通常是一个时间相关参数, 有许多标记在greenlet让您能够监视线程的状态
标记 | 解释 |
---|---|
started | Boolean, 指示是否启动了Greenlet |
ready() | Boolean, 指示greenlet是否已停止 |
successful() | Boolean, 指示此Greenlet是否已经停止而且没抛异常 |
value | 任意, 由greenlet返回值 |
exception | exception, 未捕获的异常实例被抛出到greenlet区域内 |
import gevent
def win():
return 'You win!'
def fail():
raise Exception('You fail at failing.')
winner = gevent.spawn(win)
loser = gevent.spawn(fail)
print(winner.started) # True
print(loser.started) # True
# 在greenlet中抛出异常, 仍旧会执行greenlet, 不会崩溃
try:
gevent.joinall([winner, loser])
except Exception as e:
print('This will never be reached')
print(winner.value) # 'You win!'
print(loser.value) # None
print(winner.ready()) # True
print(loser.ready()) # True
print(winner.successful()) # True
print(loser.successful()) # False
# 在失败中引发的异常,不会往上抛异常, 仍旧待在greenlet
# 堆栈跟踪将被打印输出,但它不会释放父栈。(PS: 我理解是,错误可以print输出,但是不会结束掉greenlet队列, 会执行下一个任务队列)
print(loser.exception)
# 尽管很有可能抛出异常以后,在外面也会抛出loser.exception或loser.get()
True
True
You win!
None
True
True
True
False
You fail at failing.
项目关闭(Program Shutdown)
当主程序接收到一个SIGQUIT时,未生成的Greenlets可能会使程序的执行时间比预期的长。这导致所谓的“僵尸进程”需要在Python解释器之外被杀死。
一个常见的模式是在主程序上侦听SIGQUIT事件并调用gevent。在退出前关闭。
import gevent
import signal
def run_forever():
gevent.sleep(1000)
if __name__ == '__main__':
gevent.signal(signal.SIGQUIT, gevent.kill)
thread = gevent.spawn(run_forever)
thread.join()
超时设定(Timeouts)
超时是对代码块或greenlet运行时的约束。
import gevent
from gevent import Timeout
seconds = 10
timeout = Timeout(seconds)
timeout.start()
def wait():
gevent.sleep(10)
try:
gevent.spawn(wait).join()
except Timeout:
print('Could not complete')
它们还可以与上下文管理器一起使用,在一个带有声明的语句中。
import gevent
from gevent import Timeout
time_to_wait = 5 # seconds
class TooLong(Exception):
pass
with Timeout(time_to_wait, TooLong):
gevent.sleep(10)
此外,gevent还为各种Greenlet和数据结构相关调用提供了超时参数。
import gevent
from gevent import Timeout
def wait():
gevent.sleep(2)
timer = Timeout(1).start()
thread1 = gevent.spawn(wait)
try:
thread1.join(timeout=timer)
except Timeout:
print('Thread 1 timed out')
# --
timer = Timeout.start_new(1)
thread2 = gevent.spawn(wait)
try:
thread2.get(timeout=timer)
except Timeout:
print('Thread 2 timed out')
# --
try:
gevent.with_timeout(1, wait)
except Timeout:
print('Thread 3 timed out')
# Thread 1 timed out
# Thread 2 timed out
# Thread 3 timed out
猴子补丁(Monkeypatching)
唉,我们来到了Gevent的黑暗角落。我一直避免提到猴子的修补,直到现在尝试和激发强大的协同模式。但现在该是讨论黑魔法猴子补丁的时候了。如果您注意到以上,我们已经调用过monkey.patch_socket(), 这是修改标准库的套接字库的纯粹的副作用。
import socket
print(socket.socket)
print("After monkey patch")
from gevent import monkey
monkey.patch_socket()
print(socket.socket)
import select
print(select.select)
monkey.patch_select()
print("After monkey patch")
print(select.select)
class 'socket.socket'
After monkey patch
class 'gevent.socket.socket'
built-in function select
After monkey patch
function select at 0x1924de8
Python在运行时允许大多数对象在运行时对模块进行修改。类, 甚至是方法,这通常是一个非常糟糕的想法,因为它创建了一个“隐式副作用”,如果出现问题,通常很难进行调试。然而,在极端情况下,需要使用一个库来改变Python本身的基本行为。在这种情况下,gevent能够在标准库中修补大多数阻塞系统调用,包括在套接字、ssl、线程和选择模块中进行协作。
例如,Redis python绑定通常使用常规的tcp套接字来与redis-server实例通信。简单地通过调用gevent.monkey.patch_all(),我们可以使redis绑定调度请求与我们的gevent栈的其余部分协同工作。
这使我们能够集成那些通常不会与gevent一起工作的库,而不需要编写任何一行代码。虽然猴子补丁仍然是邪恶的,但在这种情况下它是一个“有用的邪恶”。
数据结构(Data Structures)
事件(Events)
- 事件是Greenlets之间异步通信的一种形式。
import gevent
from gevent.event import Event
'''
说明事件的使用
'''
evt = Event()
def setter():
'''3秒后,唤醒所有等待evt值的线程'''
print('A: Hey wait for me, I have to do something')
gevent.sleep(3)
print("Ok, I'm done")
evt.set()
def waiter():
'''3秒后,调用将释放'''
print("I'll wait for you")
evt.wait() # 阻塞
print("It's about time")
def main():
gevent.joinall([
gevent.spawn(setter),
gevent.spawn(waiter),
gevent.spawn(waiter),
gevent.spawn(waiter),
gevent.spawn(waiter),
gevent.spawn(waiter)
])
if __name__ == '__main__':
main()
# I'll wait for you
# I'll wait for you
# I'll wait for you
# I'll wait for you
# I'll wait for you
# Ok, I'm done
# It's about time
# It's about time
# It's about time
# It's about time
# It's about time
事件对象的一个扩展是AsyncResult,它允许您通过唤醒调用发送一个值。这有时被称为未来或延期,因为它包含一个对未来值的引用,该值可以在任意时间调度中设置。
import gevent
from gevent.event import AsyncResult
a = AsyncResult()
def setter():
"""
3秒后设置a的结果
"""
gevent.sleep(3)
a.set('Hello!')
def waiter():
"""
3秒后,get调用将在setter之后打开。
将一个值放入AsyncResult中。
"""
print(a.get())
gevent.joinall([
gevent.spawn(setter),
gevent.spawn(waiter),
])
队列(Queues)
队列是有序的数据集,这些数据具有通常的put / get操作,但它们的编写方式是这样的,这样它们就可以安全地通过greenlet进行操作
例如,如果一个Greenlet从队列中取出一个项目,同样的项目将不会被另一个同时执行的Greenlet取出。
import gevent
from gevent.queue import Queue
tasks = Queue()
def worker(n):
while not tasks.empty():
task = tasks.get()
print('Worker %s got task %s' % (n, task))
gevent.sleep(0)
print('Quitting time!')
def boss():
for i in xrange(1,25):
tasks.put_nowait(i)
gevent.spawn(boss).join()
gevent.joinall([
gevent.spawn(worker, 'steve'),
gevent.spawn(worker, 'john'),
gevent.spawn(worker, 'nancy'),
])
队列也可以在put或get中阻塞
每个put和get操作都有一个非阻塞对应项,put_nowait和get_nowait不会阻塞, 然而在操作不能完成时抛出gevent.queue.Empty或gevent.queue.Full异常
在下面例子中,我们让boss与多个worker同时运行,并限制了queue不能放入多于3个元素。 这个限制意味着,直到queue有空余空间之间,put操作会被阻塞。相反地,如果队列中 没有元素,get操作会被阻塞。它同时带一个timeout参数,允许在超时时间内如果 队列没有元素无法完成操作就抛出gevent.queue.Empty异常。
import gevent
from gevent.queue import Queue, Empty
tasks = Queue(maxsize=3)
def worker(name):
try:
while True:
task = tasks.get(timeout=1) # decrements queue size by 1
print('Worker %s got task %s' % (name, task))
gevent.sleep(0)
except Empty:
print('Quitting time!')
def boss():
"""
Boss will wait to hand out work until a individual worker is
free since the maxsize of the task queue is 3.
"""
for i in xrange(1,10):
tasks.put(i)
print('Assigned all work in iteration 1')
for i in xrange(10,20):
tasks.put(i)
print('Assigned all work in iteration 2')
gevent.joinall([
gevent.spawn(boss),
gevent.spawn(worker, 'steve'),
gevent.spawn(worker, 'john'),
gevent.spawn(worker, 'bob'),
])
# Worker steve got task 1
# Worker john got task 2
# Worker bob got task 3
# Worker steve got task 4
# Worker john got task 5
# Worker bob got task 6
# Assigned all work in iteration 1
# Worker steve got task 7
# Worker john got task 8
# Worker bob got task 9
# Worker steve got task 10
# Worker john got task 11
# Worker bob got task 12
# Worker steve got task 13
# Worker john got task 14
# Worker bob got task 15
# Worker steve got task 16
# Worker john got task 17
# Worker bob got task 18
# Assigned all work in iteration 2
# Worker steve got task 19
# Quitting time!
# Quitting time!
# Quitting time!
组和池(Groups and Pools)
一个组是运行中的greenlets的集合,它们被作为组一起管理和调度。它也兼饰了像Python的multiprocessing库那样的 平行调度器的角色。
import gevent
from gevent.pool import Group
def talk(msg):
for i in xrange(3):
print(msg)
g1 = gevent.spawn(talk, 'bar')
g2 = gevent.spawn(talk, 'foo')
g3 = gevent.spawn(talk, 'fizz')
group = Group()
group.add(g1)
group.add(g2)
group.join()
group.add(g3)
group.join()
# bar
# bar
# bar
# foo
# foo
# foo
# fizz
# fizz
# fizz
这对于管理异步任务组非常有用。
正如上面所提到的,Group还提供了一个API,用于分派作业给分组的greenlets,并以各种方式收集它们的结果。
import gevent
from gevent import getcurrent
from gevent.pool import Group
group = Group()
def hello_from(n):
print('Size of group %s' % len(group))
print('Hello from Greenlet %s' % id(getcurrent()))
group.map(hello_from, xrange(3))
def intensive(n):
gevent.sleep(3 - n)
return 'task', n
print('Ordered')
ogroup = Group()
for i in ogroup.imap(intensive, xrange(3)):
print(i)
print('Unordered')
igroup = Group()
for i in igroup.imap_unordered(intensive, xrange(3)):
print(i)
# Size of group 3
# Hello from Greenlet 4340152592
# Size of group 3
# Hello from Greenlet 4340928912
# Size of group 3
# Hello from Greenlet 4340928592
# Ordered
# ('task', 0)
# ('task', 1)
# ('task', 2)
# Unordered
# ('task', 2)
# ('task', 1)
# ('task', 0)
map也分阻塞和非阻塞。
imap 与 map的区别是,map是当所有的进程都已经执行完了,并将结果返回了,那么才返回map()函数的一个list结果。
imap()则是立即返回一个iterable可迭代对象。其迭代随着进行返回的结果而逐步迭代。
imap()和 imap_unordered()的区别
- imap_unordered()不保证返回的结果顺序与进程添加的顺序一致。
池(pool)是一个为处理数量变化并且需要限制并发的greenlet而设计的结构。 在需要并行地做很多受限于网络和IO的任务时常常需要用到它。
import gevent
from gevent.pool import Pool
pool = Pool(2)
def hello_from(n):
print('Size of pool %s' % len(pool))
pool.map(hello_from, xrange(3))
Size of pool 2
Size of pool 2
Size of pool 1
当构造gevent驱动的服务时,经常会将围绕一个池结构的整个服务作为中心。 一个例子就是在各个socket上轮询的类。
from gevent.pool import Pool
class SocketPool(object):
def __init__(self):
self.pool = Pool(1000)
self.pool.start()
def listen(self, socket):
while True:
socket.recv()
def add_handler(self, socket):
if self.pool.full():
raise Exception("At maximum pool size")
else:
self.pool.spawn(self.listen, socket)
def shutdown(self):
self.pool.kill()
锁和信号量(Locks and Semaphores)
信号量是一个允许greenlet相互合作,限制并发访问或运行的低层次的同步原语。 信号量有两个方法,acquire和release。在信号量是否已经被 acquire或release,和拥有资源的数量之间不同,被称为此信号量的范围 (the bound of the semaphore)。如果一个信号量的范围已经降低到0,它会 阻塞acquire操作直到另一个已经获得信号量的greenlet作出释放。
from gevent import sleep
from gevent.pool import Pool
from gevent.coros import BoundedSemaphore
sem = BoundedSemaphore(2)
def worker1(n):
sem.acquire()
print('Worker %i acquired semaphore' % n)
sleep(0)
sem.release()
print('Worker %i released semaphore' % n)
def worker2(n):
with sem:
print('Worker %i acquired semaphore' % n)
sleep(0)
print('Worker %i released semaphore' % n)
pool = Pool()
pool.map(worker1, xrange(0,2))
pool.map(worker2, xrange(3,6))
Worker 0 acquired semaphore
Worker 1 acquired semaphore
Worker 0 released semaphore
Worker 1 released semaphore
Worker 3 acquired semaphore
Worker 4 acquired semaphore
Worker 3 released semaphore
Worker 4 released semaphore
Worker 5 acquired semaphore
Worker 5 released semaphore
范围为1的信号量也称为锁(lock)。它向单个greenlet提供了互斥访问。 信号量和锁常常用来保证资源只在程序上下文被单次使用。
线程局部变量(Thread Locals)
Gevent也允许你指定局部于greenlet上下文的数据。在内部,它被实现为以greenlet的为键,在一个私有命名空间寻址的全局查找。
import gevent
from gevent.local import local
stash = local()
def f1():
stash.x = 1
print(stash.x)
def f2():
stash.y = 2
print(stash.y)
try:
stash.x
except AttributeError:
print("x is not local to f2")
g1 = gevent.spawn(f1)
g2 = gevent.spawn(f2)
gevent.joinall([g1, g2])
# 1
# 2
# x is not local to f2
子进程(Subprocess)
自gevent 1.0起,gevent.subprocess, 一个Python subprocess模块的修补版本已经添加。它支持协作式的等待子进程。
import gevent
from gevent.subprocess import Popen, PIPE
def cron():
while True:
print("cron")
gevent.sleep(0.2)
g = gevent.spawn(cron)
sub = Popen(['sleep 1; uname'], stdout=PIPE, shell=True)
out, err = sub.communicate()
g.kill()
print(out.rstrip())
# cron
# cron
# cron
# cron
# cron
# Linux