最近用了 gevent,做个小结,理理对 gevent 的认识。
gevent is a coroutine-based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.
特点:
有几个概念需要先了解下:
需要先介绍下 进程(process)、线程(thread) 的概念,具体的概念参考维基百科中关于 进程 和 线程 的概念,这里做个简要介绍:
什么是 coroutine (协程)?
简单的理解,它是程序的 执行流,在 暂停 和 再次执行 过程中可以记录当前的状态,在 暂停 后需要 再次执行 时,可以从 暂停前的状态继续执行。coroutine 暂停执行时,可以调度到另一个 coroutine 执行,这两个 coroutine 之间的关系是对等的。
这样看来 coroutine 和 generator 的概念很像,因为后者也可以保存当前的执行状态并在再次运行时恢复之前的状态,区别在于 coroutine 暂停时可以调度到另一个 coroutine 执行,而 generator 暂停时由它的调用者继续执行。
coroutine 的调度由使用者决定,而不是像 进程(process) 和 线程(thread) 一样由操作系统进行调度。
gevent 中的 coroutine 在暂停时,会执行一个称为 hub 的 coroutine,由它选择处于等待执行状态的 coroutine 继续执行流 (可参考 scaling-real-world-applications-using-gevent 这个 slide 中 17--24 页的演示,需)。
具体的理解可参考 coroutine:wiki 和 淺談coroutine與gevent
根据 计算机组成原理 中的介绍,包括:
目前的计算机常采用后两种方式进行 I/O 控制,这样在进行 I/O 操作时,CPU 可以尽量不参与到这个过程中而去执行其它的操作。由于 I/O 比较耗时,采用 DMA 方式 和 通道方式 可以把 CPU 从 I/O 过程 "解放" 出来,提高了系统的效率。
程序运行过程中一般会遇到两种类型的 I/O,即:
这种操作一般会阻塞程序的运行,浪费 CPU 时间 (因为此时程序分配到了 时间片,在该 时间片 内程序独占 CPU 资源,但因为 I/O 被阻塞了,CPU 没有被其它程序享有而被浪费)。因此在写高性能程序时,I/O 是需要重点关注的,通过如下几种途径解决因 I/O 带来的效率问题:
大多涉及到 I/O 的高性能库一般都是重点通过第三种途径解决 I/O 时的性能问题,如 Tornado 的异步操作、gevent 基于的greenlet 的 coroutine。
gevent 通过 greenlet 实现 coroutine,创建、调度的开销比 线程(thread) 还小,因此程序内部的 执行流 效率高。
gevent 实现了 python 标准库中一些阻塞库的非阻塞版本,如 socket、os、select 等 (全部的可参考 gevent1.0 的 monkey.py 源码),可用这些非阻塞的库替代 python 标准库中的阻塞的库。
gevent 提供的 API 与 python 标准库中的用法和名称类似,减少了学习成本。
gevent 的优势:
如果程序涉及较多的 I/O,可用 gevent 替代多线程来提高程序效率。但由于
因此,gevent 不适合在以下场景中使用:
一般有两种方法:
第一种方法是调用了 Greenlet class 中的 spawn 类方法,且生成 greenlet instance 后将其放入 coroutine 的调度队列中。第二种方法需要手动通过 instance.start() 方法手动将其加入到 coroutine 的调度队列中。
具体的代码参见:test_multigreenlets.py
需要注意:
可能不太完整:
join()
方法
主要参考这篇:gevent-tutorial
以下列出些我对其中部分内容的理解:
greenlet instances 之间的关系存在两种:
第一种形式很常见,不同的 greenlet instance 之间没有交流,且没有共享数据需要进行操作,各自做各自的事情。
对于第二种形式,gevent 提供了几种数据结构便于 greenlet instances 间进行同步。
通过这种同步原语可以控制一个 greenlet 与多个 greenlet 通过 flag 的 True/False 值进行同步,Event instance 的初始 flag 是 False.但这种同步是 一次性的,即 set() 设置 flag 为 True 对之前因为 wait() 阻塞的 greenlets 都有效,但对 set() 之后再次设置wait() 的 greenlet 无效。
一般的使用场景是:一个 greenlet 控制何时通过 Event().set() 方法设置 flag 的值为 True 来控制其它协作的 greenlet 运行,有协作关系的 greenlet 通过 Event().wait() 方法等待 flag 被设为 True,否则一直阻塞。一旦 flag 被设为了 True,之前所有的Event().wait() 操作将不再阻塞,这些 greenlet 将等待 hub greenlet 进行调度,即使之后 _flag 又被设置为了 False。可参考这个 问题,还可以再对比一个程序理解这点儿,参考 这个。
若出现 wait() 处于永远的阻塞状态,即在 wait() 设置之后,在其它 greenlet 中没有 set() 操作,则 gevent 会抛出这样的异常:
1
|
gevent.hub.LoopExit: This operation would block forever
|
类似 gevent.event.Event,但在调用 set() 方法时可以传递消息,等待的 greenlet 可以通过 wait() 或 get() 方法读取 set() 设置的值。可参考 这个
这个模块中包含了几种常用的 queue,它的方法与标准库中的 Queue 类似,主要区别在于 put() 和 get() 方法中阻塞时 (对于put 是 queue 已经满了,对于 get 是 queue 已经空了) 添加了将执行权交给 hub greenlet 的操作。使用时需要注意几点:
通过 gevent.pool.Group 和 gevent.pool.Pool 可以管理一组 greenlets,前者没有容量限制,后者可设置 Pool 中 greenlet 的最大数量。当容器中的 greenlet 任务完成了,容器会自动将其从中除去。对于 Pool,容器中的 greenlet 数超过设置的最大值时,会将执行权交给 hub greenlet 进行调度。
Pool 可以通过设置最大数量来控制并发度,在爬虫中控制并发量有帮助。
有个比较有趣的现象,借此也可以 gevent 的调度和 Pool 的行为,代码在 这里,运行结果如图:
通过 gevent.spawn() 生成的 greenlet 是已经加入到调度队列中的,当 Pool 中 greenlet 数量达到最大值时,阻塞自身的 add()操作,执行权交给 hub greenlet,此时已经生成的 greenlet 都将被调度执行,当 Pool 再次被调度到时,会先检查已有的 greenlet 状态,若这些 greenlet 的 ready() 方法返回 True (即表示运行完),则删除该 greenlet,然后加入新的 greenlet。
其中 imap() 和 map() 是比较有趣的两个,参见这个问题:How does map() and imap() work in gevent.pool.Pool?。imap() 的操作是 lazy 的,即执行这条语句时不会有任何效果,只是生成了一个 greenlet iterator。当调用 map() 方法时,它会将 imap()生成的 greenlet iterator 变成 list 形式的数据,注意此时执行的动作:
通过该类,可以通过 信号量 实现 greenelets 之间的协作关系。
通过该类,可以创造些只在单个 greenlet 范围内的变量。这种作用在 web 开发中很有用,尤其是涉及到 session 的情况下。
代码参见这里: testcoroutineschedule.py
运行结果:
代码参见这里: testmainthreadcoroutines.py
运行结果:
gevent-1.0 还有些特性没有涉及到,实际使用时最好经常翻看 gevent 源码,通过 stackoverflow 查询解决办法。它的源码组织很不错,值得经常读下。
参考:
* gevent 官网
* coroutine:wiki * generator: wiki
* greenlet
* 淺談coroutine與gevent
* scaling-real-world-applications-using-gevent (需访问)
* gevent-whats-the-point (需访问)
* IO-bound
* CPU-bound