Python中的线程处理


76

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


我认为这个问题至少有三个原因(建议,建议和过于广泛)被搁置。
lpapp 2014年

我知道这个问题来自09,但有人可以回答asyncio吗?在此先感谢您,并希望看到代码的外观。
alvas 2015年

@lpapp那里的问题产生了一些自以为是的答案,我相信它的措辞是客观的(也许太宽泛了)。OP正在询问Python中使用了哪些模块来进行并发,以及每个模块的利弊。对我来说听起来很合理。
Guyarad

Answers:


116

为了增加复杂度:

使用穿线模块

优点:

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

缺点:

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

使用多重处理模块

在简单的用例中,threading除了每个任务都是在自己的进程而不是自己的线程中运行之外,这与使用完全一样。(几乎从字面上看:如果以Eli的示例为例,并threadingmultiprocessing,,替换ThreadProcess,然后将Queue(模块)替换为multiprocessing.Queue,它应该可以正常运行。

优点:

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

缺点:

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

使用事件模型,例如Twisted

优点:

  • 您可以很好地控制优先级,执行时间。

缺点:

  • 即使有了一个好的库,异步编程通常也比线程编程难,无论是在理解应该发生的事情还是在调试实际发生的事情上,难度都很高。

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


4
我认为您的复杂性排序几乎完全是倒退。多线程编程确实很难正确完成(几乎没有人这样做)。事件编程是不同的,但是容易理解正在发生的事情并编写测试来证明它已完成了应有的工作。(我说这个周末在大型并发网络库上实现了100%的覆盖率)。
达斯丁2009年

3
嗯 我认为事件编程暴露了复杂性。它迫使您更直接地处理它。您可以辩称,复杂性是并发性固有的,无论您如何处理它,我都同意。但是,我已经完成了一些相当大的线程化和基于事件的程序,我认为我支持我所说的:基于事件的程序在我的控制之下要多得多,但是实际编写它却更加复杂。
夸克

1
什么进程比线程慢。是什么意思?进程不能比线程慢,因为进程不运行任何代码。这些进程中的线程运行代码。我只能猜测这意味着启动过程较慢,这是正确的。因此,如果您的程序需要频繁启动新线程/进程,则使用起来multiprocessing会较慢。但是,在这种情况下,您的多线程使用率也可以大大提高。
guyarad

103

从“伪线程”一直到外部框架,您已经获得了各种各样的答案,但是我还没有看到有人提及Queue.QueueCPython线程的“秘密秘诀”。

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

“忘记”共享内存,据说是线程与多处理的主要结合-效果不好,扩展性不好,永远不会,永远不会。使用只适用于设置一次数据结构,共享内存之前的一切,做一个-你产卵子线程,并没有改变事后单一线程负责该资源的同时,通过该线程进行通信Queue

为您通常认为要通过锁保护的每个资源分配一个专用线程:可变数据结构或其内聚组,与外部进程(DB,XMLRPC服务器等)的连接,外部文件等,等等。获得一个小的线程池来执行没有或不需要那种专用资源的通用任务-不要在需要时按需生成线程,否则线程切换开销将使您不堪重负。

两个线程之间的通信始终是通过Queue.Queue-消息传递的形式进行的,这是多处理的唯一理智的基础(除了事务内存之外,这很有希望,但我知道除了In Haskell,没有其他有价值的实现)。

每个管理单个资源(或一组小的内聚资源)的专用线程都侦听特定Queue.Queue实例上的请求。池中的线程在一个共享的Queue.Queue上等待(队列具有可靠的线程安全性,不会在此失败)。

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

您还可以使用Queue来“驻留”资源的实例,这些实例可以由任何一个线程使用,但绝不能一次在多个线程之间共享(具有某些DBAPI组件的DB连接,具有其他游标的游标等等)-这使您可以放松支持更多池化的专用线程要求(从共享队列中获取需要可排队资源的请求的池线程将从适当队列中获取该资源,并在必要时等待,等等)。

Twisted实际上是组织此小对象(或视情况而定的广场舞)的一种好方法,这不仅要归功于延迟,而且由于其健全,扎实,高度可扩展的基础体系结构:只有在以下情况下,您才可以安排使用线程或子进程的事物真正有保证,同时在一个事件驱动的线程中执行通常认为线程有价值的大多数事情。

但是,我意识到Twisted并非适合所有人-“专用或池资源,使用wazoo排队,永远不需要做任何需要Lock的操作,或者Guido禁止,任何更高级的同步过程,例如信号量或条件”方法都可以即使您只是无法用异步事件驱动的方法来处理问题,它仍然会被使用,并且仍然会提供比我偶然发现的任何其他广泛应用的线程方法更高的可靠性和性能。


6
如果您喜欢一个答案,我会为此提供答案。这是我在Stack Overflow上发现的最发人深省的内容之一。
夸克

1
@quark,感谢您的客气话,很高兴您喜欢它!
Alex Martelli,2009年

4
希望我能翻番给予好评本
科里·戈德堡

2
这是关于线程的一个强有力的一般答案:所描述的方法反映了从其他多处理语言中选择的方法,包括scala(LinkedBlockingQueue作为Actors的后备构造)和smalltalk。值得一读。
StephenBoesch 2013年

亚历克斯(Alex)(不是真的回答问题,但值得一读)。在大多数内容看起来都是不朽的地方(即,即使您写了大约7年前,今天的内容也是如此),我想知道您的观点此后发生了什么变化。当然可以随意链接到其他资源...
Guyarad

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()

1
抱歉,如果我想将一个返回值作为目标传递给Thread的可调用对象,如何在主线程中获取其结果?是否可以或者应该对函数使用包装器,以将其结果放入可修改的对象中?我不想将结果绑定到线程对象本身。最佳做法是什么?谢谢。
newtover 2010年

3
@newtover:您仍在描述与我的示例相同的基本生产者/消费者线程情况,因此在这种情况下,Pythonic解决方案仍将使用同步的Queue对象。让每个线程将其结果放入输出值队列中,并让主线程在空闲时从队列中检索它们。可以在docs.python.org/library/queue.html上找到Queue类的文档,甚至还提供了一个示例,可以完全按照docs.python.org/library/queue.html#Queue.Queue.join的
伊莱·考特赖特

1
感谢您的链接和答案。还有一个问题:是否有类似字典的功能相同,还是最好从队列中获取所有项目并自己填写字典?我可以为此目的使用内置字典,自己加入线程吗?
newtover 2010年

2
@newtover:Python规范不保证dict可以同步,但是CPython实现可以。因此,除非您使用的是Jython或PyPy或IronPython之类的东西,否则您可能可以使用普通的dict,具体取决于您的操作。因此,如果只是用不同的线程来设置字典键/值,那就可以了。但如果你是在一个字典或迭代读取/修改/重新设置字典值,那么你可能需要做自己的同步,这样的:docs.python.org/library/...
伊莱Courtwright

1
我的任务意味着并行化映射值计算,而无需在中间进行任何其他映射处理。我使用内置的字典。您确认dict的CPython实现已同步,因此我将继续解决该问题。再一次感谢你。
newtover 2010年

13

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

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

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

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

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

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


1
我不知道为什么有人会将此答案记为下标...请将投票否决...除非您能提供一个很好的理由将其记下...
乔恩

15
我猜有些人讨厌猫
Sam Hasler

6

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

从根本上讲,它提供了一个具有收件箱和发件箱的运行中事物的隐喻。您将邮件发送到发件箱,并将其连接在一起时,邮件将从发件箱流到收件箱。无论您是使用生成器,线程或进程,还是与其他系统对话,此隐喻/ API均保持不变。

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

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

Pipeline(Producer(), Consumer() )

在此示例中,这些是线程化组件还是其他无关紧要,从使用角度来看,它们之间的唯一区别是该组件的基类。生成器组件使用列表进行通信,使用Queue.Queues使用线程化的组件进行通信,并使用os.pipes进行基于进程的通信。

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

如果您在任何地方使用裸线程并进行锁定,那么通常会假设您在编写代码时不会犯任何错误。我们都渴望实现这一目标,但这种情况很少发生。通过将锁定行为包装在一个地方,可以简化可能出错的地方。(上下文处理程序有帮助,但对于上下文处理程序外的意外更新则无济于事)

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

相关链接:

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

我能理解为什么其他Kamaelia答案被修改了,因为对我来说,它看起来更像是广告,而不是答案。作为Kamaelia的作者,很高兴看到热情,尽管我希望它包含更多相关的内容:-)

这就是我的说法,请注意,这个答案在定义上是有偏差的,但是对我而言,Kamaliaa的目标是尝试并总结IMO的最佳实践。我建议尝试一些系统,然后看看哪个对您有用。(同样如果这不适合堆栈溢出,抱歉-我是这个论坛的新手:-)


3

如果必须使用线程,我将使用无堆栈Python的微线程(任务集)。

整个在线游戏(主要是多人游戏)都是基于Stackless及其多线程原理构建的-因为最初的版本只是为了降低游戏的大规模多人游戏属性。

不鼓励使用CPython中的线程。原因之一是GIL(全局解释器锁),它在执行的许多部分对线程进行序列化。我的经验是,用这种方法创建快速应用程序确实很困难。在我的示例代码中,使用线程的速度都很慢-一个内核(但是多次等待输入应该可以提高性能)。

对于CPython,请尽可能使用单独的进程。


3

如果您真的想弄脏手,可以尝试使用生成器来伪造协程。就所涉及的工作而言,它可能不是最高效的,但是协程确实为您提供了对协作多任务的非常好的控制,而不是您会在其他地方发现的抢先式多任务。

您会发现一个优点是,使用协作多任务处理时,基本上不需要锁或互斥体,但是对我而言,更重要的优点是“线程”之间的切换速度几乎为零。当然,据说无堆栈Python对此也非常有用。然后是Erlang(如果不是必须使用Python)。

协作式多任务处理中的最大缺点可能是,普遍缺乏阻止I / O的解决方法。在伪造的协程中,您还将遇到一个问题,即您不能从线程中堆栈顶层以外的任何内容切换“线程”。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.