我也在学习如何threading在multiprocessingPython 中使用模块来并行运行某些操作并加速我的代码。
我很难理解 objectthreading.Thread()和multiprocessing.Process().
此外,我并不完全清楚如何实例化一个作业队列,并且只有 4 个(例如)并行运行,而其他的则等待资源在执行前被释放。
我发现文档中的示例清晰但不是很全面。一旦我尝试使事情复杂化一点,我就会得到很多奇怪的错误。
那么,我什么时候应该使用模块threading和multiprocessing?
您能否将我链接到一些资源来解释这两个模块背后的概念以及如何正确地将它们用于复杂的任务?
一个进程中可以有多个线程。属于同一进程的线程使用相同的内存区域(可以读取和写入相同的变量并且可以相互干扰)。相反,不同的进程存在于不同的内存区域,每个进程都有自己的变量。进程必须使用其他通道(文件、管道或套接字)进行通信。
如果你想并行计算,你可能需要多线程,因为你可能希望线程在同一个内存中交互。
说到性能,线程的创建和管理比进程更快(因为操作系统不必分配全新的虚拟内存区域),而且线程间通信通常比进程间通信更快。但是线程更难编程。线程可以相互干扰并且可以写入彼此的内存,但是这种发生的方式并不总是很明显(由于几个因素,主要是指令重新排序和内存缓存),因此您需要同步原语来控制对变量的访问.
成员@GiulioFranco的回答翻译
Giulio Franco所说的对于一般的多线程和多处理来说是正确的。
然而,Python* 还有一个额外的问题:它有一个全局解释器锁,可以防止同一进程中的两个线程同时执行 Python 代码。这意味着如果你有 8 个核心,而你将代码更改为使用 8 个线程,它将无法使用 800% 的 CPU 并运行速度快 8 倍;它将使用相同的 100% CPU 并以相同的速度运行。(它实际上会运行得慢一点,因为即使您没有共享数据,线程也会带来额外的开销,但现在请忽略它。)
这也有例外。如果您的代码的大量计算实际上并没有发生在 Python 中,而是在某些具有正确处理 GIL 的自定义 C 代码的库中发生,例如 numpy 应用程序,您将从线程中获得预期的性能优势。如果繁重的计算是由您启动并等待的某个子进程完成的,情况也是如此。
更重要的是,有些情况并不重要。例如,网络服务器将大部分时间用于从网络读取数据包,而 GUI 应用程序将大部分时间用于等待用户事件。在网络服务器或 GUI 应用程序中使用线程的一个原因是允许您运行长时间运行的“后台任务”,而不会阻止主线程继续为网络数据包或 GUI 事件提供服务。它适用于 Python 线程。(从技术上讲,这意味着 Python 线程即使不提供内核并行性也提供并发性。)
但是如果你用纯 Python 编写一个 CPU 密集型程序,使用更多线程通常没有帮助。
使用单独的进程不会导致 GIL 出现此类问题,因为每个进程都有自己单独的 GIL。当然,线程和进程之间仍然存在与任何其他语言相同的权衡——进程之间的通信比线程之间的通信更加困难和昂贵,运行大量进程,或者频繁创建和销毁数据等。但是 GIL 对进程有很大的影响,而 C 或 Java 则不然。因此,您会发现自己在 Python 中比在 C 或 Java 中更多地使用多处理。
同时,Python 的“包含电池”理念带来了好消息:编写可以在线程和进程之间切换的代码非常容易,只需更改一行代码。
如果您根据与输入和输出以外的其他作业(或主程序)无关的自包含“作业”来设计代码,则可以使用
concurrent.futures库围绕线程池编写代码,如下所示:您甚至可以获取这些作业的结果并将它们传递给其他作业,按执行顺序或完成顺序等待某些东西等;阅读更多内容
Future objects现在,如果事实证明您的程序一直在使用 100% 的 CPU,并且添加更多线程只会减慢它的速度,那么您遇到了 GIL 问题,因此您需要切换到进程。您所要做的就是更改第一行:
唯一真正需要注意的是,您的作业的参数和返回值必须是可处理的(并且不会花费太多时间或内存来处理),以便它们可以在不同的进程中使用。通常这不是问题,但有时会发生。
但是,如果您的工作无法离线怎么办?如果您可以根据将消息从一个传递到另一个的作业来设计您的代码,那么它仍然非常容易。您可能需要使用
threading.Thread或multiprocessing.Process不依赖于池。而且您将需要显式创建对象queue.Queue或multiprocessing.Queue(还有许多其他选项 - 管道、套接字、集群文件……但最重要的是,如果自动魔术Executor还不够,您需要手动执行某些操作。)但是,如果您甚至不能依赖消息传递怎么办?如果您需要两个工作来改变相同的结构并看到彼此发生变化怎么办?在这种情况下,您将需要进行手动同步(锁、信号量、条件等),如果您想使用进程,则需要显式加载共享内存对象。当多线程(或多处理)成为一项艰巨的任务时,就会发生这种情况。如果可以避免,那就太好了;如果你不能,你需要阅读的不仅仅是关于 SO 的答案。
从评论中您想知道 Python 中线程和进程之间的区别。实际上,如果您阅读 Giulio Franco 的回答、我的和我们所有的链接,它们应该涵盖所有内容......但总结肯定会有所帮助,所以这里是:
默认情况下,线程通信;没有进程。
作为 (1) 的结果,在进程之间传输数据通常需要对它们进行酸洗和解压缩。**
作为 (1) 的另一个结果,进程之间直接共享数据通常需要将其置于低级格式,例如
Value、Array和ctypes。流程不受 GIL 约束。
在某些平台(主要是 Windows)上,创建和销毁进程的成本要高得多。
进程有一些额外的限制,其中一些在平台上是不同的。有关详细信息,请参阅编程指南。
该模块
threading没有某些模块功能multiprocessing。(您可以使用multiprocessing.dummy在流之上获取大部分缺少的 API,或者您可以使用更高级别的模块concurrent.futures,而不用担心它。)*实际上并不是 Python 有这个问题,而是 CPython,该语言的“标准”实现。Jython 等其他一些实现没有 GIL。
**如果您使用fork start方法进行多处理 - 这可以在大多数非 Windows 平台上完成 - 每个子进程都会获得父进程在子进程启动时拥有的所有资源,这可能是另一种将数据传递给子进程的方式。
成员@abarnert的回答翻译