老实说多线程是很有挑战性的-我们已经在上一节中看到了。事实上对问题的最简单的方法是只需要最小的代价。但是以一种安全的方式处理线程需要大量的代码。我们必须设置线程池和通信队列优雅地处理来自线程的异常并且在尝试提供速率限制功能时也考虑线程安全。十行代码只能从外部库并行执行一个函数我们假设它可以用于生产环境因为有外部包创建者的承诺它的库是线程安全的。听起来像一个高价格的解决方案实际上它只适用于执行 I/O 绑定任务。实现并行性的另一种方法是多进程。彼此独立 Python 进程没有 GIL 的限制这样可以有更好的资源利用率。这对于在多核处理器上运行的应用程序尤其重要这些处理器可以真正的处理 CPU 密集型任务。现在这是为 Python 开发人员提供的唯一内置并行解决方案使用 CPython 解释器你可以从多个处理器核心中受益。使用多个进程的另一个优点是它们不共享内存上下文。因此很难破坏数据也难以在应用程序中引入死锁。不共享内存上下文意味着你需要一些额外的努力在隔离的进程之间传递数据但幸运的是有许多好的方法来实现可靠的进程间通信。事实上Python 提供了一些原语使进程之间的通信与线程之间的一样简单。在任何编程语言中启动新进程的最基本的方法通常是在某个时刻派生程序。在 POSIX系统Unix、Mac OS 和 Linux上派生是通过 os.fork()函数在 Python 中暴露的系统调用它将创建一个新的子进程。然后两个进程在派生后自己继续该程序。以下是一个示例脚本它自己派生一次import ospid_list []def main():pid_list.append(os.getpid())child_pid os.fork()if child_pid 0:pid_list.append(os.getpid())print()print(“CHLD: hey, I am the child process”)print(“CHLD: all the pids i know %s” % pid_list)else:pid_list.append(os.getpid())print()print(“PRNT: hey, I am the parent”)print(“PRNT: the child is pid %d” % child_pid)print(“PRNT: all the pids i know %s” % pid_list)ifname “main”:main()以下是一个在终端中运行它的例子$ python3 forks.pyPRNT: hey, I am the parentPRNT: the child is pid 21916PRNT: all the pids i know [21915, 21915]CHLD: hey, I am the child processCHLD: all the pids i know [21915, 21916]注意这两个进程在 os.fork()调用之前它们的数据具有完全相同的初始状态。它们都具有与 pid_list 集合的第一个值相同的 PID 号进程标识符。后来两个状态发生了分歧我们可以看到子进程添加了 21916 值而父进程复制了它的 21915 PID。这是因为这两个进程的内存上下文不共享。它们具有相同的初始条件但在 os.fork()调用后不能相互影响。派生将内存上下文复制到子进程后每个进程都会处理自己的地址空间。为了沟通进程需要与系统范围的资源或使用低级工具如信号。不幸的是os.fork 在 Windows 下不可用需要生成一个新的解释器以模仿 fork功能。所以它根据不同的平台会有差别。os 模块还暴露了函数它可以在 Windows 下生成新进程但最终你很少使用它们。os.fork()也是如此。Python 提供了一个很好的multiprocessing 模块为多进程创建了一个高级接口。这个模块的最大优点是它提供了一些抽象这些抽象针对我们必须从头开始编写一个多线程应用的例子。它可以限制样板代码的数量从而提高应用程序可维护性并降低其复杂性。令人惊讶的是尽管它的名称multiprocessing 模块也暴露了类似的线程接口所以你可能想要使用相同的接口来实现两种方法。内置的 multiprocessing 模块multiprocessing 提供了一种便捷的方式来处理进程就像它们是线程一样。此模块包含一个与 Thread 类非常相似的 Process 类可以在任何平台上使用from multiprocessing import Processimport osdef work(identifier):print(‘hey, i am a process {}, pid: {}’‘’.format(identifier, os.getpid()))def main():processes [Process(targetwork, args(number,))for number in range(5)]for process in processes:process.start()while processes:processes.pop().join()ifname “main”:main()上述脚本在执行时会输出以下结果$ python3 processing.pyhey, i am a process 1, pid: 9196hey, i am a process 0, pid: 8356hey, i am a process 3, pid: 9524hey, i am a process 2, pid: 3456hey, i am a process 4, pid: 6576当创建进程时内存被派生在 POSIX 系统上。最有效的进程用法是让它们在创建后自己工作以避免开销并从主线程检查它们的状态。除了被复制的内存状态之外Process 类还在其构造函数中提供了一个额外的 args 参数以便传递数据。进程模块之间的通信需要一些额外的工作因为它们的本地内存在默认情况下不共享。为了简化这一点multiprocessing 模块提供了进程之间的几种通信方式• 使用 multiprocessing.Queue 类它是早先用于线程之间通信的 queue.Queue的近似克隆。• 使用 multiprocessing.Pipe这是一个类似于套接字的双向通信通道。• 使用 multiprocessing.sharedctypes 模块通过它可以在进程之间共享的专用内存池中创建任意 C 类型从 ctypes 模块。multiprocessing.Queue 和 queue.Queue 类具有相同的接口。唯一的区别是第一个是设计用于多进程环境而不是多个线程所以它使用不同的内部传输和锁定原语。我们已经在一个多线程应用的例子中了解了如何在多线程中使用 Queue因此我们不会用多进程执行相同的操作。使用保持完全相同所以这样的例子不会带来任何新的内容。现在一个更有趣的模式是由 Pipe 类提供的。它是一个双工双向通信通道在概念上非常类似于 Unix 管道。管道的接口也非常类似于来自内置 socket 模块的简单套接字。与原始系统管道和套接字的区别在于你可以发送任何可选对象使用 pickle 模块而不仅是原始字节。这使得进程之间可以更容易的通信因为你几乎可以发送任何基本的Python 类型如下所示from multiprocessing import Process, Pipeclass CustomClass:passdef work(connection):while True:instance connection.recv()if instance:print(“CHLD: {}”.format(instance))else:returndef main():parent_conn, child_conn Pipe()child Process(targetwork, args(child_conn,))for item in (42,‘some string’,{‘one’: 1},CustomClass(),None,):print(“PRNT: send {}:”.format(item))parent_conn.send(item)child.start()child.join()ifname “main”:main()当查看上述脚本的示例输出时你将看到你可以轻松地传递自定义类实例并且它们具有不同的地址具体取决于进程如下所示PRNT: send: 42PRNT: send: some stringPRNT: send: {‘one’: 1}PRNT: send: main.CustomClass object at 0x101cb5b00PRNT: send: NoneCHLD: recv: 42CHLD: recv: some stringCHLD: recv: {‘one’: 1}CHLD: recv: main.CustomClass object at 0x101cba400另一种在进程之间共享状态的方法是在 multiprocessing.sharedctypes 中提供的类中使用共享内存池中的原始类型。最基本的是 Value 和 Array。下面是 multiprocessing模块的官方文档中的示例代码from multiprocessing import Process, Value, Arraydef f(n, a):n.value 3.1415927for i in range(len(a)):a[i] -a[i]ifname ‘main’:num Value(‘d’, 0.0)arr Array(‘i’, range(10))p Process(targetf, args(num, arr))p.start()p.join()print(num.value)print(arr[:])此示例将打印以下输出3.1415927[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]使用 multiprocessing.sharedctypes 时你需要记住你正在处理共享内存或其他进程间通信通道。在大多数情况下避免共享类型是合理的因为它们增加代码复杂性并带来多线程中已知的所有危险。