gevent 小结

最近用了 gevent,做个小结,理理对 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.

特点:

  • Lightweight execution units based on greenlet
  • Monkey patching utility to get 3rd party modules to become cooperative
  • API that re-uses concepts from the Python standard library (for example there are Events and Queues)
  • Fast event loop based on libev (epoll on Linux, kqueue on FreeBSD)
  • Cooperative sockets with SSL support
  • DNS queries performed through threadpool or c-ares.

有几个概念需要先了解下: 

coroutine

需要先介绍下 进程(process)线程(thread) 的概念,具体的概念参考维基百科中关于 进程 和 线程 的概念,这里做个简要介绍:

  • 它们都是操作系统中的模型,操作系统通过这两种模型执行程序
  • 进程(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 方式

根据 计算机组成原理 中的介绍,包括:

  • 程序查询方式
  • 程序中断方式
  • DMA 方式
  • 通道方式

目前的计算机常采用后两种方式进行 I/O 控制,这样在进行 I/O 操作时,CPU 可以尽量不参与到这个过程中而去执行其它的操作。由于 I/O 比较耗时,采用 DMA 方式 和 通道方式 可以把 CPU 从 I/O 过程 "解放" 出来,提高了系统的效率。

程序运行过程中一般会遇到两种类型的 I/O,即:

  • 当前机器的磁盘 I/O
  • 网络 I/O

这种操作一般会阻塞程序的运行,浪费 CPU 时间 (因为此时程序分配到了 时间片,在该 时间片 内程序独占 CPU 资源,但因为 I/O 被阻塞了,CPU 没有被其它程序享有而被浪费)。因此在写高性能程序时,I/O 是需要重点关注的,通过如下几种途径解决因 I/O 带来的效率问题:

  • 减少 I/O 次数 (即优化程序的结构,把需要读写的数据汇聚在一起一次性读写)
  • 提高硬件 I/O 速度 (如使用 SSD 硬盘)
  • I/O 时不阻塞当前的执行流,由 DMA 控制器 或 通道 负责 I/O 操作,CPU 继续执行程序的其它部分或执行其它程序

大多涉及到 I/O 的高性能库一般都是重点通过第三种途径解决 I/O 时的性能问题,如 Tornado 的异步操作、gevent 基于的greenlet 的 coroutine


对 gevent 特性的理解

gevent 通过 greenlet 实现 coroutine,创建、调度的开销比 线程(thread) 还小,因此程序内部的 执行流 效率高。

gevent 实现了 python 标准库中一些阻塞库的非阻塞版本,如 socket、os、select 等 (全部的可参考 gevent1.0 的 monkey.py 源码),可用这些非阻塞的库替代 python 标准库中的阻塞的库。

gevent 提供的 API 与 python 标准库中的用法和名称类似,减少了学习成本。


什么时候使用/不使用 gevent

gevent 的优势:

  • 可以通过同步的逻辑实现并发操作,大大降低了编写并行/并发程序的难度
  • 在一个进程中使用 gevent 可以有效避免对 临界资源 的互斥访问

如果程序涉及较多的 I/O,可用 gevent 替代多线程来提高程序效率。但由于

  • gevent 中 coroutine 的调度是由使用者而非操作系统决定
  • 主要解决的是 I/O 问题,提高 IO-bound 类型的程序的效率
  • 由于是在一个进程中实现 coroutine,且操作系统以进程为单位分配处理机资源 (一个进程分配一个处理机)

因此,gevent 不适合在以下场景中使用:

  • 对任务延迟有要求的场景,如交互式程序中 (此时需要操作系统进行 公平调度)
  • CPU-bound 任务
  • 当需要使用多处理机时 (可通过运行多个进程,每个进程内实现 coroutine 来解决这个问题)


gevent 操作

如何生成 greenlet instance

一般有两种方法:

  • 使用 gevent.spawn() API
  • subclass Greenlet

第一种方法是调用了 Greenlet class 中的 spawn 类方法,且生成 greenlet instance 后将其放入 coroutine 的调度队列中。第二种方法需要手动通过 instance.start() 方法手动将其加入到 coroutine 的调度队列中。
具体的代码参见:test_multigreenlets.py

需要注意:

  • 若仅是想生成 greenlet instance 并置于调度队列中,最好采用 gevent.spawn() API
  • 若想仅生成 greenlet instance 且暂时不想加入到调度队列,则可采用第二种方法。之后若想将其加入到调度队列,则手动执行 instance.start() 方法。

如何进行主线程到 hub greenlet instance 的切换

可能不太完整:

  • gevent.sleep()
  • Greenlet 或 Greenlet 子类的 instance 的 join() 方法
  • monkey patch 的库或方法 (参见 monkey.py):
    • socket
    • ssl
    • os.fork
    • time.sleep
    • select.select
    • thread
    • subprocess
    • sys.stdin,sys.stdout,sys.stderr


gevent 实践

主要参考这篇:gevent-tutorial

以下列出些我对其中部分内容的理解: 
greenlet instances 之间的关系存在两种:

  • 仅有包含于 greenlet instances 集合的关系
  • 同步关系,即存在协作关系

第一种形式很常见,不同的 greenlet instance 之间没有交流,且没有共享数据需要进行操作,各自做各自的事情。
对于第二种形式,gevent 提供了几种数据结构便于 greenlet instances 间进行同步。

gevent.event.Event

通过这种同步原语可以控制一个 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.AsyncResult

类似 gevent.event.Event,但在调用 set() 方法时可以传递消息,等待的 greenlet 可以通过 wait() 或 get() 方法读取 set() 设置的值。可参考 这个

gevent.queue

这个模块中包含了几种常用的 queue,它的方法与标准库中的 Queue 类似,主要区别在于 put() 和 get() 方法中阻塞时 (对于put 是 queue 已经满了,对于 get 是 queue 已经空了) 添加了将执行权交给 hub greenlet 的操作。使用时需要注意几点:

  • put() 阻塞执行 (默认情况) 时,若 queue 未满,则装入,否则将执行权交给 hub greenlet,实现 并发。若之后没有代码执行 get() 操作,则 put() 操作会抛出异常,指出它会无限等待。get() 阻塞执行 (默认情况) 时与之类似。
  • putnowait() 与标准库中的做法一样,若 queue 未满,则装入,否者抛出 Full 异常。getnowait() 与之类似。
  • put() 与 putnowait() 的区别在于如何对待 queue 满的情况,前者等待,后者直接抛出异常。get() 与 getnowait() 的区别与之类似。

gevent.pool

通过 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 形式的数据,注意此时执行的动作:

  • 通过 IMap (继承自 Greenlet) 调用 Pool 自身的 spawn() 方法生成一个个 greenlet instance
  • 调用 Pool 自身的 start() 方法将生成的 greenlet 加入到 self.greenlets 变量中
  • 再将这些 greenlets 加入到等待调度的队列中。这个过程中进入了 greenlet 的调度过程 (这点儿还不是很清楚,这部分的源码没有分析透彻)。

gevent.lock.BoundedSemaphore

通过该类,可以通过 信号量 实现 greenelets 之间的协作关系。

gevent.local.local

通过该类,可以创造些只在单个 greenlet 范围内的变量。这种作用在 web 开发中很有用,尤其是涉及到 session 的情况下。


一些验证想法的代码

验证自己对 gevent 中 greenlets 调度顺序的理解

代码参见这里: testcoroutineschedule.py
运行结果:

验证自己对主线程到 hub greenlet 的切换和 greenlets 间的切换

代码参见这里: 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

你可能感兴趣的:(programming)