在 Python 的发展历史中,有过一些失败的修复 CPython 的缺陷和提高性能的尝试,比如消除 GIL、Stackless(一个微线程扩展,避免传统线程所带来的性能与复杂度问题)、psyco (被 PyPy 代替)、 Unladen Swallow 。当然也有成功的,比如 PyPy。

协程

在开始表达我的观点之前,我们先来了解下 Coroutine。

Coroutine 也就是 corporate routine,直译为「协同的例程」,中文一般叫做「协程」, 实际上这个概念和进程与线程有相似之处,因为 linux 线程就是所谓的「轻量级进程」。

我在 gevent 源码分析 中找到一段表述的比较好的描述进程和协程异同的内容:

  1. 相同点:

    二者都是可以看做是一种执行流,该执行流可以挂起,并且在将来又可以在 你挂起的地方恢复执行,这实际上都可以看做是 continuation, 我们来看看当我们挂 起一个执行流时我们要保存的东西:

    1. 栈,因为如果你不保存栈,那么局部变量你就无法恢复,同时函数的调用链你也无 法恢复,
    2. 寄存器的状态:这好理解,比如说 EIP, 如果你不保存,那么你恢复执行流就不知道 到底执行哪一条指令,在比如说 ESP,EBP, 如果你不保存,那么你即便有完整的栈 你也不知道怎么用. 这二者实际就是所谓的上下文,也可以说是 continuation. 在执行流切换时必须保存 这两个东西,内核调度进程时也是一回事.
  2. 不同点:

    1. 执行流的调度者不同,进程是内核调度,而协程是在用户态调度,也就是说进程 的上下文是在内核态保存恢复的,而协程是在用户态保存恢复的。很显然用户态的 代价更低
    2. 进程会被抢占,而协程不会,也就是说协程如果不主动让出 CPU, 那么其他的协程是不 可能得到执行机会,这实际和早期的操作系统类似,比如 DOS, 它有一个 yield 原语,一个进程调用 yield, 那么它就会让出 CPU, 其他的进程也就有机会执行了,如果一 个进程进入了死循环,那么整个系统也就挂起了,永远无法运行其他的进程了,但 对协程而言,这不是问题
    3. 对内存的占用不同,实际上协程可以只需要 4K 的栈就够了,而进程占用的内存要大 的多.
    4. 从操作系统的角度讲,多协程的程序是单线程,单进程的

那用一句话描述协程的优势就是由开发者决定协程的切换,操作系统无法干预切换,且占用内存小的多

Gevent 是一种基于协程的 Python 网络库,它用到 Greenlet 提供的,封装了 libevent 事件循环的高层同步 API。它让开发者在不改变编程习惯的同时,用同步的方式写异步 I/O 的代码。

在 12-13 年的时候,我也用过 gevent 做过一些爬虫、网络编程的工作。在我使用场景中,使用 Gevent 的性能确实要比用传统的线程高,甚至高很多。

但是发现 Gevent 直到现在也仍然受到国人的喜欢( 给 Gevent 点赞的程序员大概一半是国人 )。虽然我在《Python Web 开发实战》一书中也有专门的一节介绍 Gevent,但是我却不建议大家在生产环境中用它(但是我不反对协程)。

现在每天在 Github 上都会出现有意思的新的项目,每个新的项目都号称解决了 XXX 问题,带来了 YY 的思路,令人激动和振奋... 但一般人都不知道它最后会不会大火,也不知道它最后会不会无疾而终。我也曾经在这些不断新的知识和项目的冲击下迷失,渐渐的总结了一些有用的判断标准:

  1. 看项目社区的繁盛情况。
  2. 看 Python 社区和核心开发者对它的态度,比如在公开场合赞同 / 反对,给项目贡献代码等。
  3. 看业界有没有真的「大牛」站队,义务宣传它甚至在走否复杂和高负荷的生产环境使用。

如果你愿意花时间,你会知道 Gvanrossum 从来不喜欢 Gevent,而是更愿意另辟蹊径的实现 asyncio (基于生成器的协程)。我找几个链接:

  1. Why not coroutines?
  2. The definitive answer (according to @glyph) on why not gevent.
  3. Async I/O for Python 3

Gvanrossum 提到了我大部分赞同,剩下的无感也是由于理解不够深入或者还「没踩过坑」。列举下我的观点:

  1. Monkey-patching。中文「猴子补丁」,常用于对测试环境做一些 hack。我个人不太喜欢这种「黑魔法」,因为如果其他人不了解细节,极为容易产生困惑。Gvanrossum 说用它就是 "patch-and-pray",太形象了。由于 Gevent 直接修改标准库里面大部分的阻塞式系统调用,包括 socket、ssl、threading 和 select 等模块,而变为协作式运行。但是我们无法保证你在复杂的生产环境中有哪些地方使用这些标准库会由于打了补丁而出现奇怪的问题,那么你只能祈祷(pray)了。其次,在 Python 之禅中明确说过:「Explicit is better than implicit.」,猴子补丁明显的背离了这个原则。最后,Gvanrossum 说 Stackless 之父 Christian Tismer 也赞同他。 我喜欢显式的「yield from」
  2. 第三方库支持。得确保项目中用到其他用到的网络库也必须使用纯 Python 或者明确说明支持 Gevent,而且就算有这样的第三方库,我还会担心这个第三方库的代码质量和功能性。
  3. Greenlet 不支持 Jython 和 IronPython,这样就无法把 gevent 设计成一个标准库了。

之前是没有选择,很多人选择了 Gevent,而现在明确的有了更正统的、正确的选择:asyncio(下一篇会详细介绍)。所以建议大家放弃 Gevent,拥抱 asyncio。

BTW,《Python 之禅》还有一句「practicality beats purity」,如果我上面说的这些问题你都有能力解决,或者知道现在以及未来不会给你造成困扰,那么用 Gevent 也是可以的。