Python中的线程编程
在Python中,写多线程应用程序通常会用到哪些模块呢?我知道语言本身提供了一些基本的并发机制,还有一个叫做Stackless Python的东西,但它们各自的优缺点是什么呢?
7 个回答
这要看你想做什么,不过我个人比较喜欢直接使用标准库里的 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()
你已经看到很多不同的回答,从“假线程”到外部框架,但我没看到有人提到 Queue.Queue
—— 这是 CPython 线程的“秘密武器”。
进一步说,只要你不需要同时进行纯 Python 的 CPU 密集型处理(在这种情况下你需要用 multiprocessing
—— 不过它也有自己的 Queue
实现,所以在一些必要的注意事项下,你可以应用我给出的通用建议;-),Python 内置的 threading
就可以用了……但如果你能合理使用它,它会表现得更好,比如这样。
先“忘掉”共享内存,这本来是线程相对于多进程的主要优点——它并不好用,扩展性也差,从来没有好过,也不会好。共享内存只适合那些在你创建子线程之前设置好且之后不再改变的数据结构——对于其他情况,最好让一个 单独 的线程来管理那个资源,并通过 Queue
与这个线程进行沟通。
为每个你通常会想用锁来保护的资源专门分配一个线程:可变的数据结构或其紧密相关的组、与外部进程的连接(数据库、XMLRPC 服务器等)、外部文件等等。为那些没有或不需要专用资源的通用任务建立一个小的线程池——不要根据需要随便创建线程,否则线程切换的开销会让你吃不消。
两个线程之间的沟通总是通过 Queue.Queue
—— 这是一种消息传递的方式,是多进程的唯一合理基础(除了事务内存,这个概念很有前景,但我知道的只有 Haskell 有值得生产使用的实现)。
每个专门管理单个资源(或小的相关资源集合)的线程会在特定的 Queue.Queue 实例上监听请求。线程池中的线程会在一个共享的 Queue.Queue 上等待(Queue 是非常安全的,不会让你失望)。
只需要在某个队列(共享或专用)上排队请求的线程可以不等待结果,直接继续执行。那些最终需要结果或确认的线程会排队一个对(请求,接收队列),并使用他们刚创建的 Queue.Queue 实例,最终,当响应或确认是继续执行的必要条件时,他们会从接收队列中获取(等待)。确保你准备好接收错误响应以及真实的响应或确认(顺便说一下,Twisted 的 deferred
非常擅长组织这种结构化的响应!)。
你还可以使用 Queue 来“停放”那些可以被任何一个线程使用但不能同时被多个线程共享的资源实例(某些 DBAPI 组件的数据库连接、其他的游标等)——这让你可以放宽专用线程的要求,转而使用更多的资源池(一个池线程从共享队列中获取需要可排队资源的请求时,会从适当的队列中获取那个资源,必要时等待等等)。
Twisted 实际上是组织这种小舞步(或者说方舞)的一种好方法,不仅因为它的 deferreds,还因为它稳固、可靠、高度可扩展的基础架构:你可以安排在真正需要时才使用线程或子进程,而在大多数通常认为需要线程的事情中,使用单个事件驱动的线程。
不过,我明白 Twisted 并不适合每个人——“专用或池化资源,尽量多用 Queue,绝不要做任何需要锁的事情,或者更高级的同步程序,比如信号量或条件”的方法,即使你无法理解异步事件驱动的方法论,仍然可以使用,并且会比我见过的任何其他广泛适用的线程方法提供更可靠和更好的性能。
按照复杂程度从低到高排列:
使用 线程模块
优点:
- 在自己的线程中运行任何函数(实际上是任何可调用的东西)非常简单。
- 共享数据虽然不算简单(锁的使用从来都不简单 :),但至少比较容易。
缺点:
- 正如Juergen提到的,Python的线程实际上不能同时访问解释器中的状态(有一个大锁,著名的全局解释器锁)。这意味着在实际操作中,线程适合处理I/O密集型任务(比如网络、写入磁盘等),但对于并发计算就没什么用处了。
使用 多进程模块
在简单的使用场景中,这看起来和使用 threading
一样,只不过每个任务是在自己的进程中运行,而不是在自己的线程中运行。(几乎可以说:如果你拿Eli的例子,把 threading
替换成 multiprocessing
,把 Thread
替换成 Process
,把 Queue
(模块)替换成 multiprocessing.Queue
,就可以正常运行。)
优点:
- 所有任务都能真正并发(没有全局解释器锁)。
- 可以扩展到多个处理器,甚至可以扩展到多台机器。
缺点:
- 进程的速度比线程慢。
- 进程之间的数据共享比线程要复杂。
- 内存不会自动共享。你要么得显式共享,要么得把变量序列化后再传来传去。这虽然更安全,但也更麻烦。(如果这很重要,越来越多的Python开发者似乎在推动人们朝这个方向发展。)
使用事件模型,比如 Twisted
优点:
- 你能对优先级和执行顺序进行非常细致的控制。
缺点:
- 即使使用好的库,异步编程通常也比线程编程要难,既难以理解应该发生什么,也难以调试实际发生了什么。
在所有情况下,我假设你已经理解了多任务处理中的许多问题,特别是如何在任务之间共享数据这个棘手的问题。如果你不清楚何时以及如何使用锁和条件,那就得先从这些基础知识开始。多任务代码充满了微妙之处和陷阱,最好在开始之前对这些概念有一个好的理解。