Python 中的线程

用 Python 编写多线程应用程序的模块是什么?我了解该语言和 无栈巨蟒提供的基本并发机制,但是它们各自的优点和缺点是什么?

64954 次浏览

这取决于您想要做什么,但是我倾向于只使用标准库中的 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()

Kamaelia 是一个用于构建具有许多通信进程的应用程序的 Python 框架。

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

在 Kamaelia,你从 ABc0开始构建系统。这加速了开发,大量的辅助维护,也意味着你 构建自然并发的软件。它的目的是为 任何开发人员,包括新手访问。这也让它变得有趣:)

什么样的系统?网络服务器、客户端、桌面应用程序、基于 pygame 的游戏、代码转换系统和管道、数字电视系统、垃圾邮件消除器、教学工具,以及相当多的其他东西:)

这是2009年 Pycon 的一段视频。它首先将卡马利亚与 扭曲并行 Python进行比较,然后亲手示范 Kamaelia。

易于与 Kamaelia 并发-第1部分 (59:08)
易于与 Kamaelia 并发-第2部分 (18:15)

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

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

CPython 中的线程普遍不鼓励使用。其中一个原因就是 GIL ——一个 GIL ——为执行的许多部分序列化线程。我的经验是,以这种方式创建快速应用程序真的很困难。我的示例编码中,所有的线程都比较慢——只有一个核心(但是许多等待输入的情况使得一些性能提升成为可能)。

对于 CPython,如果可能的话,应该使用独立的进程。

如果你真的想弄脏你的手,你可以试试 用发电机伪造协同程序。就所涉及的工作而言,它可能不是最有效的,但协同程序确实能够很好地控制 合作多任务,而不是你在其他地方会发现的先发制人的多任务。

您将发现的一个优势是,在使用协作多任务时,基本上不需要锁或互斥锁,但对我来说更重要的优势是“线程”之间的切换速度几乎为零。当然,据说 Stackless Python 对此也很有帮助,还有 Erlang,如果它不是 Python 的话。

协同多任务最大的缺点可能是缺乏阻塞 I/O 的解决方案。在伪协同程序中,你也会遇到这样的问题: 除了在线程中的堆栈顶层,你不能从任何地方切换“线程”。

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

按照日益复杂的顺序:

使用 螺纹装配模块螺纹装配模块

优点:

  • 中运行任何函数(实际上是任何可调用的)都非常容易 自己的线。
  • 共享数据即使不容易(锁从来都不容易:) ,也是如此 最不简单的。

缺点:

  • 正如前面提到的 作者: Juergen Python 线程实际上不能并发访问解释器中的状态(有一个大锁,臭名昭著的 GIL)在实践中,这意味着线程对于 I/O 绑定任务(联网、写入磁盘等)很有用,但对于执行并发计算一点用都没有。

使用 多重处理模块

在简单的用例中,这看起来与使用 threading完全一样,只不过每个任务都在自己的进程中运行,而不是在自己的线程中运行。(几乎是字面上的意思: 如果您使用 伊莱的例子,并将 threading替换为 multiprocessingThreadProcessQueue(模块) ,那么它应该可以正常运行。)

优点:

  • 所有任务的实际并发性(无 GIL)。
  • 可以扩展到多个处理器,甚至可以扩展到多个 机器

缺点:

  • 进程比线程慢。
  • 进程之间的数据共享比线程之间的数据共享更加棘手。
  • 内存不是隐式共享的。您要么必须显式地共享它,要么必须使用 pickle 变量并来回发送它们。这样更安全,但更难。(如果这越来越重要的话,Python 开发人员似乎正在把人们推向这个方向。)

使用事件模型,如 扭曲

优点:

  • 您可以极好地控制优先级,控制什么时候执行。

缺点:

  • 即使使用好的库,异步编程通常也比线程编程更难,无论是在理解应该发生什么和调试实际发生什么方面都很难。

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

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

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

“不完美”部分是由于收件箱和发件箱还没有添加语法糖(尽管这正在讨论中)-系统中有一个安全性/可用性的焦点。

以生产者消费者使用裸线程为例,Kamaelia 的情况是这样的:

Pipeline(Producer(), Consumer() )

在本例中,这些组件是否是线程化组件并不重要,从使用角度来看,它们之间的唯一区别是组件的基类。生成器组件使用列表进行通信,线程组件使用 Queue 进行通信。基于使用 os.tube 的队列和进程。

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

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

显然,并不是每一段代码都可以写成消息传递和共享风格,这就是为什么 Kamaelia 还有一个简单的软件事务内存(STM) ,这是一个非常好的想法,有一个讨厌的名字——它更像变量的版本控制——即检查一些变量,更新它们,然后提交回来。如果出现冲突,就冲洗并重复。

相关连结:

不管怎样,我希望这是个有用的答案。FWIW,Kamaelia 设置背后的核心原因是使并发在 Python 系统中更安全和更容易使用,而不是像狗一样摇尾巴。(装满零件的大桶)

我能理解为什么另一个卡马利亚答案被修改了,因为即使对我来说,它看起来更像一个广告而不是答案。作为《卡马利亚》的作者,很高兴看到读者的热情,不过我希望这篇文章能包含更多相关内容: -)

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

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

展开: 只要你不需要重叠纯 Python CPU 处理(在这种情况下,你需要 multiprocessing——但它也有自己的 Queue实现,所以你可以在一些需要注意的地方应用我给出的一般建议; —— Python 的内置 threading会做... 但如果你使用它 是经过深思熟虑的,它会做得更好,例如,如下所示。

“忘记”共享内存,理应是线程对多处理的主要优势——它不能很好地工作,不能很好地伸缩,从来没有,将来也不会。共享内存只用于在 之前产生子线程之后建立的数据结构,而且此后不会更改——对于其他任何事情,让一个 单身线程负责该资源,并通过 Queue与该线程通信。

为每一个你通常认为需要用锁来保护的资源分配一个专门的线程: 一个可变的数据结构或其内聚组,一个到外部进程的连接(数据库、 XMLRPC 服务器等) ,一个外部文件等等。建立一个小型的线程池,用于那些没有或不需要专用资源的通用任务——在需要时产生 不要线程,否则线程切换开销将使您不堪重负。

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

管理单个资源(或小型内聚资源集)的每个专用线程侦听特定 Queue 上的请求。队列实例。池中的线程等待单个共享队列。Queue (Queue 是完全线程安全的,不会在这方面使您失败)。

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

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

Twist 实际上是组织这首小步舞曲(或者方舞曲)的一个好方法,这不仅归功于延迟,而且归功于它健全、坚实、高度可伸缩的基础架构: 你可以安排事情使用线程或子进程,只有在真正有必要的时候,同时在一个事件驱动的线程中做大多数通常认为值得线程的事情。

但是,我意识到 Twsted 并不适合所有人——“专用或共享资源,使用队列,永远不要做任何需要 Lock 的事情,或者,Guido 禁止,任何更高级的同步过程,如信号量或条件”的方法仍然可以使用,即使你只是不能理解异步事件驱动的方法,仍然会提供更多的可靠性和性能比任何其他广泛适用的线程方法,我曾经偶然发现。