Python 并发模型

399 查看

Python 的 Threads 、 Microthreads(Tasklets) 和 Greenlets 的区别比较

最近我注意到很多 Python 论坛上的问题在询问关于线程(Threads),微线程(Microthread)和绿色线程(Greenthread)这几个并发模型之间的具体差异是什么。问题诸如:

  • 它们在实现上有何差异?
  • 微线程/绿色线程 也有 常规线程 那样的数量上限吗?
  • 它们每个与其它相比的优缺点是什么?我应该使用哪一个?

线程

离开并发模型这个集合,让我们先从最熟知的线程开始。在Python中,使用规范的Posix线程来实现线程。即是,在Python中每个线程映射到一个系统级的线程,并且系统内核察觉和负责维护这些线程。包括线程运行时的抢占,调度线程的下一个时隙,也包括处理上下文交换(和CPU另外一个寄存器交换线程状态等等。)

线程的特征就是,你运行得越多,内核调度器在同一时间应对的任务就越多。当你有太多的线程时,性能就会削弱,因为每个线程获得的执行时间片段,变得和线程间交换所需时间可以相比拟——交换开销成为了主要的瓶颈。使用线程,你需要保持运行的数量在一个合理的数量,100或者更少(运行一个线程池是一种用法例子)

因为每个Python线程都被内核所映射和管理,当一个上下文交换发生的时候,在用户空间(大多数用户程序花费它们时间的地方)到内核空间的来回交换中,存在额外的开销。同样这也是一个相对昂贵的操作,这也导致了同时运行太多线程的问题。

尽管线程被认为是轻量的,我们将会看到存在更加轻量的选择。

微线程(小任务)

无栈Python项目(重点修改了Python解释器的核心以形成兼容fork的Python)以小任务的名字(在介绍其他新特性中)介绍了微线程。

这个项目对线程的处理办法是对内核隐藏线程,并且由Python解释器自身处理所有的调度和上下文切换。从历史的观点来看,这对一个本身不支持线程的操作系统来说,通过对虚拟机或者解释器增加线程支持是非常有用的。(这种方法在Solaris 操作系统上的Java 1.1曾经用过,Solaris OS那时不支持线程)。甚至在操作系统本身支持线程的情况下,这种方法也有几个优点。即线程之间切换管理花费得到非常大的减少-不再执行用户空间到内核空间切换和反向切换。无栈Python通过解释器既处理了微线程调度也处理了其上下文切换。

尽管在性能方面有某些优点,无栈Python项目仍然是一个与主线代码无关的独立项目。这种情况出现有几个原因(你可以从这儿阅读到有关它的信息),原因之一就是其中的更改并不是很细小的,并且这种更改破坏了几个Python扩展。除非你的代码运行在你可以控制所用解释器的机器上,否则你就可能打算坚持使用Python的参考实现而避免使用微线程。不过,还有一种方法使用一般的Python解释器获得无栈Python的某些优点—继续向下阅读…

Greenlets

而对于Python解释器,微线程需要较大的修改,Greenlets是微线程的一个分支,并且能够通过Python扩展来安装(Stackless太复杂而无法成为一个扩展)。Greenlets的思想实际上是从Stackless项目中提取出来的,并且保留了相似的优势,例如相对于内核,在解释器中管理线程。然而也存在一个主要的区别——Greenlets的一个实例没有明显的调度安排。

缺少一个调度程序意味着,你能够完全控制一个Greenlets的实例何时转向另一个。这就是众所周知的协作并发模型,即是为了在不同的Greenlets的实例之间进行转换,每个Greenlets的实例都必须自愿地放弃它的执行。而对于微线程,当转换(也即是创建)的时候只有非常低的开销,因为这个原因,你可以有大量的,和线程相关的Greenlets的实例。

协作并发模型的一个优点就是它的确定性的本质。你非常确定地知道一个Greenlet实例在哪里退出,另外一个在哪里开始。这容许你在处理竞态条件的时候,避免在共享数据结构上使用锁。

如果你思考一下,你会注意到Greenlets是伪并发(即使在一个没有GIL的解释器中)——这就意味着不可能存在多个greenlet在运行,并且它仅仅是程序中表示流程的一种方式。使用大量的if/else模块结构和一些循环,可以模拟仿真Greenlet的行为,但是这种方式显然不是非常简洁的。

附加注释:如果你的greenlet在转向另一个greenlet之前,碰到了一个阻塞方法的调用,那么将会发生什么?你是程序进程将会被迫暂停,直到阻塞调用返回。有许多非常不错的库,可以帮助你逃开上述问题。如果你存在I/O阻塞(套接字和文件)调用,你可以考虑使用GEvent,它提供greenlets并且能够将I/O调用修改为无阻塞。更多内容请猛戳

比较

在最后一节,我提到了Greenlets是伪并发的,亦即在一个给定的时间只有一个是实际在运行的。所以这就是说与微线程和线程比较起来,Greenlets处于劣势对吗?好吧理论上说是的,但对Python不是。关于Python和并发的问题就是声名狼藉的GIL(全局解释器锁),这使得多于一个的线程/微线程不能同时运行。当你的代码运行于Python解释器时,你无法获得多处理器系统的优势。所以那使Greenlets同微线程和常规的线程处于同样的基础之上。现在问题更多的变成了,当运行具有许多线程的大量进程切换时,你是否需要高性能?当你的greenlets放弃运行时,你是否准备好了动手并精确控制?Greenlets是你的解决方案。

另一方面,如果你没有较高的性能要求,希望运行多个线程并让系统为你调度它们(不要忘记你需要锁!),可以考虑使用线程。尽管因为 greenlets 更轻量而具有诱惑力,但在许多情况下,它让你感觉不出有什么实际意义。比如我目前的工作就在使用普通的线程,因为线程和抢先式切换模型也能很好解决这个问题,而且我并不需要很高的性能。

当然,若你愿意安装一个解析器的修改版,Stackless 是可选之一,通过它,你可以象隐式调度一样方便获得 Greenlets 的益处。

还有一个我之前没提到的潜在解决方案。如果你有一个多核处理器,在Python中利用它的优势的唯一途径是使用进程。Python对进程提供的API几乎和线程API是一样的,相似的地方结束。每个进程都由自己的Python解释器启动,这意味着你避免了 GIL 麻烦。想让你的系统最大化的使用,你需要启动与你CPU内核相同数量的进程。请注意,因为你的代码在不同的进程运行,他们不能访问到对方的变量,所以一些与顺序有关的通信方法需要额外的设计(有其自身性能缺陷)。

对于Python的并发,没有一个一刀切的选择。应根据你的实际情况仔细推敲每一个的好处,选择最适合你的方式。