77

在 Python 中用于编写多线程应用程序的模块有哪些?我知道该语言以及Stackless Python提供的基本并发机制,但是它们各自的优缺点是什么?

4

7 回答 7

119

按照复杂度递增的顺序:

使用线程模块

优点:

  • 在自己的线程中运行任何函数(实际上是任何可调用的)真的很容易。
  • 共享数据即使不容易(锁从来都不容易:),至少很简单。

缺点:

  • 正如Juergen所提到的, Python 线程实际上不能同时访问解释器中的状态(有一个大锁,臭名昭著的Global Interpreter Lock。)这在实践中意味着线程对于 I/O 绑定任务(网络、写入磁盘、等等),但对进行并发计算毫无用处。

使用多处理模块

在简单的用例中,这看起来与 using 完全一样,threading只是每个任务都在自己的进程中运行,而不是在自己的线程中运行。(几乎从字面上看:如果您以Eli 的示例为例threading,并用multiprocessingThreadProcess和替换Queue(模块)用multiprocessing.Queue,它应该运行得很好。)

优点:

  • 所有任务的实际并发(无全局解释器锁)。
  • 扩展到多个处理器,甚至可以扩展到多台机器

缺点:

  • 进程比线程慢。
  • 进程之间的数据共享比线程更棘手。
  • 内存不是隐式共享的。您要么必须明确共享它,要么必须腌制变量并来回发送它们。这更安全,但更难。(如果它越来越重要,Python 开发人员似乎正在推动人们朝这个方向发展。)

使用事件模型,例如Twisted

优点:

  • 您可以非常精细地控制优先级,以及何时执行什么。

缺点:

  • 即使有一个好的库,异步编程通常也比线程编程更难,在理解应该发生的事情和调试实际发生的事情方面都很难。

所有情况下,我假设您已经了解多任务处理所涉及的许多问题,特别是如何在任务之间共享数据的棘手问题。如果由于某种原因您不知道何时以及如何使用锁和条件,您必须从这些开始。多任务代码充满了微妙之处和陷阱,最好在开始之前对概念有一个很好的理解。

于 2009-07-27T21:53:28.357 回答
104

你已经得到了各种各样的答案,从“假线程”一直到外部框架,但我看到没有人提到Queue.Queue——CPython 线程的“秘密武器”。

扩展:只要您不需要重叠纯 Python CPU 繁重的处理(在这种情况下您需要multiprocessing- 但它也有自己的Queue实现,所以您可以在需要注意的情况下应用我的一般建议'm give;-),Python 的内置程序会做......但如果你明智threading地使用它,它会做得更好,例如,如下所示。

“忘记”共享内存,据说是线程与多处理的主要优势——它不能很好地工作,它不能很好地扩展,永远不会,永远不会。仅将共享内存用于在生成子线程之前设置一次并且之后从未更改过的数据结构- 对于其他所有内容,使单个线程负责该资源,并通过Queue.

为您通常认为通过锁保护的每个资源分配一个专用线程:可变数据结构或其内聚组、与外部进程(数据库、XMLRPC 服务器等)的连接、外部文件等. 为没有或需要此类专用资源的通用任务准备一个小型线程池——不要在需要时生成线程,否则线程切换开销会让你不知所措。

两个线程之间的通信总是通过Queue.Queue——一种消息传递的形式,是多处理的唯一健全的基础(除了事务内存,这是有希望的,但我知道除了在 Haskell 中没有生产价值的实现)。

每个管理单个资源(或小的内聚资源集)的专用线程侦听特定 Queue.Queue 实例上的请求。池中的线程在单个共享 Queue.Queue 上等待(队列是可靠的线程安全的,不会让您失望)。

只需要在某个队列(共享或专用)上排队请求的线程无需等待结果即可继续执行。最终确实需要请求结果或确认的线程将一对(请求,接收队列)与他们刚刚创建的 Queue.Queue 实例排队,最终,当响应或确认对于继续进行必不可少时,他们得到(等待) 从他们的接收队列中。确保您已准备好获得错误响应以及真实的响应或确认(Twisteddeferred非常擅长组织这种结构化的响应,顺便说一句!)。

您还可以使用 Queue 来“停放”任何一个线程可以使用但永远不会同时在多个线程之间共享的资源实例(与某些 DBAPI 组件的 DB 连接、与其他组件的游标等)——这让您放松支持更多池的专用线程要求(从共享队列获取需要可排队资源的请求的池线程将从适当的队列中获取该资源,必要时等待等)。

Twisted 实际上是组织这个小步舞曲(或广场舞,视情况而定)的好方法,这不仅要归功于 deferreds,还因为它健全、可靠、高度可扩展的基础架构:您可以安排事情以使用线程或子进程,仅当确实有保证,同时在单个事件驱动线程中执行通常认为值得线程执行的大多数事情。

但是,我意识到 Twisted 并不适合所有人——“专用或池化资源,使用 WAZOO 排队,从不做任何需要锁的事情,或者,Guido 禁止,任何更高级的同步过程,例如信号量或条件”方法可以即使您无法完全理解异步事件驱动的方法,仍然可以使用它,并且仍然可以提供比我偶然发现的任何其他广泛适用的线程方法更高的可靠性和性能。

于 2009-07-28T05:43:42.203 回答
22

这取决于你想要做什么,但我偏爱只使用threading标准库中的模块,因为它可以很容易地获取任何函数并在单独的线程中运行它。

from threading import Thread

def f():
    ...

def g(arg1, arg2, arg3=None):
    ....

Thread(target=f).start()
Thread(target=g, args=[5, 6], kwargs={"arg3": 12}).start()

等等。Queue我经常使用模块提供的同步队列设置生产者/消费者

from Queue import Queue
from threading import Thread

q = Queue()
def consumer():
    while True:
        print sum(q.get())

def producer(data_source):
    for line in data_source:
        q.put( map(int, line.split()) )

Thread(target=producer, args=[SOME_INPUT_FILE_OR_SOMETHING]).start()
for i in range(10):
    Thread(target=consumer).start()
于 2009-07-27T19:44:32.687 回答
13

Kamaelia是一个 python 框架,用于构建具有大量通信进程的应用程序。

(来源:kamaelia.org Kamaelia - 并发变得有用、有趣

在 Kamaelia 中,您可以使用相互通信的简单组件构建系统。这加快了开发速度,极大地帮助了维护,还意味着您可以构建自然并发的软件。它旨在供任何开发人员访问,包括新手。它也让它变得有趣:)

什么样的系统?网络服务器、客户端、桌面应用程序、基于 pygame 的游戏、转码系统和管道、数字电视系统、垃圾邮件根除器、教学工具等等 :)

这是来自 Pycon 2009 的视频。它首先将 Kamaelia 与TwistedParallel Python进行比较,然后动手演示 Kamaelia。

使用 Kamaelia 轻松并发 - 第 1 部分(59:08)
使用 Kamaelia 轻松并发 - 第 2 部分(18:15)

于 2009-07-27T19:46:55.583 回答
6

关于 Kamaelia,上面的答案并没有真正涵盖这里的好处。Kamaelia 的方法提供了一个统一的接口,但实用性并不完美,用于在单个系统中处理线程、生成器和进程以实现并发。

从根本上说,它提供了一个有收件箱和发件箱的正在运行的东西的隐喻。您将消息发送到发件箱,当连接在一起时,消息从发件箱流向收件箱。无论您是使用生成器、线程或进程,还是与其他系统对话,这个比喻/API 都保持不变。

“不完美”的部分是由于尚未为收件箱和发件箱添加语法糖(尽管这正在讨论中) - 关注系统中的安全性/可用性。

以上面使用裸线程的生产者消费者示例为例,这在 Kamaelia 中变成了这样:

Pipeline(Producer(), Consumer() )

在这个例子中,如果这些是线程组件无关紧要,从使用的角度来看,它们之间的唯一区别是组件的基类。生成器组件使用列表进行通信,线程组件使用 Queue.Queues 和基于进程的使用 os.pipes。

这种方法背后的原因是使调试错误变得更加困难。在线程或您拥有的任何共享内存并发中,您面临的第一个问题是意外破坏了共享数据更新。通过使用消息传递,您可以消除类错误。

如果您在任何地方都使用裸线程和锁,那么您通常会假设您在编写代码时不会犯任何错误。虽然我们都渴望这样做,但这种情况很少发生。通过将锁定行为封装在一个地方,您可以简化可能出错的地方。(上下文处理程序有帮助,但对上下文处理程序之外的意外更新没有帮助)

显然,并非每段代码都可以编写为消息传递和共享样式,这就是为什么 Kamaelia 也有一个简单的软件事务内存 (STM),这是一个非常巧妙的想法,但名字却很讨厌——它更像是变量的版本控制——即检查一些变量,更新它们并提交回来。如果发生冲突,请冲洗并重复。

相关链接:

无论如何,我希望这是一个有用的答案。FWIW,Kamaelia 设置背后的核心原因是让并发在 python 系统中更安全、更容易使用,而不会摇尾巴。(即大桶组件

我可以理解为什么另一个 Kamaelia 答案被修改了,因为即使对我来说,它看起来更像是一个广告而不是一个答案。作为 Kamaelia 的作者,很高兴看到热情,但我希望这包含更多相关内容:-)

这就是我的说法,请注意这个答案在定义上是有偏见的,但对我来说,Kamaelia 的目标是尝试总结什么是 IMO 最佳实践。我建议尝试一些系统,看看哪个适合你。(如果这不适合堆栈溢出,对不起 - 我是这个论坛的新手 :-)

于 2009-07-27T22:36:52.527 回答
4

如果我必须使用线程,我会使用 Stackless Python 的微线程(Tasklets)。

一个完整的在线游戏(大型多人游戏)是围绕 Stackless 及其多线程原理构建的——因为原始游戏只是为了游戏的大型多人游戏属性而减慢速度。

CPython 中的线程被广泛反对。一个原因是 GIL - 一个全局解释器锁 - 为执行的许多部分序列化线程。我的经验是,以这种方式创建快速应用程序真的很困难。我的示例编码在使用线程时都变慢了——只有一个内核(但许多等待输入应该可以提高一些性能)。

使用 CPython,如果可能,宁愿使用单独的进程。

于 2009-07-27T20:27:36.157 回答
3

如果你真的想弄脏你的手,你可以尝试使用生成器来伪造协程。就所涉及的工作而言,它可能不是最有效的,但是协同程序确实为您提供了对协作式多任务处理的非常精细的控制,而不是您在其他地方可以找到的先发制人的多任务处理。

您会发现一个优势是,总的来说,在使用协作多任务时您不需要锁或互斥锁,但对我来说更重要的优势是“线程”之间的切换速度几乎为零。当然,据说 Stackless Python 也非常适合。然后是 Erlang,如果它不必Python 的话。

协作式多任务处理的最大缺点可能是通常缺乏阻塞 I/O 的解决方法。在伪造的协程中,您还会遇到无法从线程内堆栈的顶层以外的任何地方切换“线程”的问题。

在您使用假协程制作了一个稍微复杂的应用程序之后,您将真正开始欣赏在操作系统级别进行进程调度的工作。

于 2009-07-27T20:49:17.300 回答