我正在学习如何使用 threading
和 multiprocessing
Python 中的模块以并行运行某些操作并加速我的代码。
我发现这很难(可能是因为我没有任何理论背景)来理解 threading.Thread()
之间的区别。对象和 multiprocessing.Process()
一。
此外,我并不完全清楚如何实例化一个作业队列并且只有 4 个(例如)它们并行运行,而另一个在执行之前等待资源释放。
我发现文档中的例子很清楚,但不是很详尽;一旦我尝试将事情复杂化,我就会收到很多奇怪的错误(例如无法腌制的方法等等)。
那么,我什么时候应该使用 threading
和 multiprocessing
模块?
您能否将我链接到一些解释这两个模块背后的概念以及如何正确使用它们来完成复杂任务的资源?
最佳答案
What Giulio Franco says一般而言,多线程与多处理都是如此。
但是,Python* 有一个附加问题:有一个全局解释器锁,可防止同一进程中的两个线程同时运行 Python 代码。这意味着如果您有 8 个内核,并且将您的代码更改为使用 8 个线程,它将无法使用 800% 的 CPU 并且运行速度提高 8 倍;它将使用相同的 100% CPU 并以相同的速度运行。 (实际上,它的运行速度会慢一点,因为线程会产生额外的开销,即使您没有任何共享数据,但现在先忽略它。)
也有异常(exception)。如果您的代码的繁重计算实际上不是在 Python 中发生,而是在一些具有自定义 C 代码的库中进行适当的 GIL 处理,如 numpy 应用程序,您将从线程中获得预期的性能优势。如果繁重的计算是由您运行并等待的某个子进程完成的,情况也是如此。
更重要的是,有些情况下这无关紧要。例如,网络服务器花费大部分时间从网络读取数据包,而 GUI 应用程序花费大部分时间等待用户事件。在网络服务器或 GUI 应用程序中使用线程的原因之一是允许您执行长时间运行的“后台任务”,而无需停止主线程继续为网络数据包或 GUI 事件提供服务。这对于 Python 线程来说工作得很好。 (从技术角度来说,这意味着 Python 线程可以提供并发性,即使它们不提供核心并行性。)
但是,如果您正在用纯 Python 编写 CPU 密集型程序,则使用更多线程通常没有帮助。
使用单独的进程对 GIL 没有这样的问题,因为每个进程都有自己单独的 GIL。当然,线程和进程之间的权衡仍然与任何其他语言相同——在进程之间共享数据比在线程之间共享数据更困难、更昂贵,运行大量进程或创建和销毁的成本可能很高它们经常出现,等等。但是 GIL 对进程的平衡有很大的影响,这种方式对于 C 或 Java 来说并非如此。因此,您会发现自己在 Python 中比在 C 或 Java 中更频繁地使用多处理。
同时,Python 的“包含电池”的理念带来了一些好消息:编写代码非常容易,只需进行一次更改即可在线程和进程之间来回切换。
如果您根据自包含的“作业”设计代码,除了输入和输出外不与其他作业(或主程序)共享任何内容,您可以使用 concurrent.futures
库围绕线程池编写代码,如下所示:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
您甚至可以获取这些作业的结果并将它们传递给进一步的作业,按执行顺序或完成顺序等待事物等;阅读
Future
上的部分对象的详细信息。现在,如果事实证明您的程序一直在使用 100% 的 CPU,并且添加更多线程只会使它变慢,那么您就会遇到 GIL 问题,因此您需要切换到进程。您所要做的就是更改第一行:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
唯一真正需要注意的是,您的作业的参数和返回值必须是可腌制的(并且不会花费太多时间或内存来腌制)才能跨进程使用。通常这不是问题,但有时是。
但是,如果您的工作不能自给自足怎么办?如果您可以根据将消息从一个传递到另一个的作业来设计您的代码,那仍然很容易。您可能必须使用
threading.Thread
或 multiprocessing.Process
而不是依赖池。你必须创建 queue.Queue
或 multiprocessing.Queue
对象明确。 (还有很多其他选项——管道、套接字、文件群……但关键是,如果 Executor 的自动魔法不足,你必须手动做一些事情。)但是,如果您甚至不能依靠消息传递呢?如果你需要两个工作来改变相同的结构,并看到彼此的变化怎么办?在这种情况下,您将需要进行手动同步(锁、信号量、条件等),并且如果您想使用进程,则需要显式共享内存对象来引导。这是多线程(或多处理)变得困难的时候。如果你能避免它,那太好了;如果您不能,您将需要阅读的内容比某人可以放入 SO 答案中的多。
从评论中,您想知道 Python 中的线程和进程之间有什么不同。真的,如果您阅读 Giulio Franco 的回答和我的以及我们所有的链接,那应该涵盖所有内容……但是摘要肯定会很有用,所以这里是:
ctypes
类型。 threading
模块没有 multiprocessing
的一些功能模块。 (您可以使用 multiprocessing.dummy
在线程之上获取大部分缺失的 API,或者您可以使用更高级别的模块,如 concurrent.futures
而不必担心。)* 实际上,存在此问题的不是 Python,即语言,而是 CPython,即该语言的“标准”实现。其他一些实现没有 GIL,例如 Jython。
** 如果您使用的是 fork多进程的 start 方法——你可以在大多数非 Windows 平台上——每个子进程在子进程启动时获取父进程拥有的任何资源,这可以是将数据传递给子进程的另一种方式。
关于python - 线程和多处理模块之间有什么区别?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18114285/