第三方库异步IO库 gevent

协程是一中多任务实现方式,它不需要多个进程或线程就可以实现多任务。


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

猜你喜欢

转载自blog.csdn.net/qq351469076/article/details/79102509