
在进行多进程编程的时候, 它要求你对进程管理、竞争条件以及锁机制这些系统级别的底层概念有特别深入的理解。虽然说这一部分内容确实是比较复杂的, 但是只要你能够掌握多进程技术的话, 那你就可以充分地去利用现代计算机里面的那种多核CPU的性能, 从而编写出那种运行起来效率非常高的应用程序。我们接下来将通过 8 个由浅入深的实例, 一步步来展开讲解, 这样可以帮助大家轻松地理解并且有效地应用多进程技术。第1级: 明白该如何去建立起一个新的进程, 这是需要首先掌握的基本内容。在进行多进程编程的时候, 模块是我们的好帮手。尽管不同的操作系统可能有着不一样的进程创建方式, 也拥有各自独特的进程管理方法, 但是, 模块已经把常见的操作都封装好了。因此, 在大多数情况下, 我们只需要使用该模块提供的接口就可以。我们不需要再去关心操作系统底层的那些机制细节。例如以下代码片段通过 模块中的 对象从主进程派生出一个名为 p 的新进程os.() 这个方法是用来拿到进程的身份编号的。就像前面那个例子展示的那样, 程序里面启动了两个进程。在处理多个进程的时候, start() 和 join() 这两个方法通常是经常用到的:在第2级的情况下, 是选择使用多个进程来并行执行多个任务的。如上所示, 因为是通过建立四个进程来进行操作, 并且让这些进程去并行运行那个空括号标记的函数, 所以程序的总执行时间是可以被缩短到 1 秒的。属于第三级的情况, 是借助池中管理的方式来让流程的管理变得简化。Pool类提供了这样一种方式, 这种方式更方便于管理多个进程的情况发生, 同时也能够进行并行执行的操作。上面的代码用了 Pool 这个东西, 弄出了跟以前一样的功能, 靠着它就不用写那么多 for 循环了。在代码里面, 大家是用 with 这个上下文管理器的办法, 搞出了一个能同时用好几个进程的池子。要是直接去建这种池子呢, 就得惦记着把它关掉, 不然可能会有问题。第 4 级使用 Pipe 实现一对一的进程间通信当两个进程需要进行消息交换的时候, Pipe 这款工具会显得非常合适, 它位于某个模块之中。它允许两个进程通过一个双向的通信通道直接进行数据交换。前面那个代码展示的内容里, 有一个叫作 Pipe 的对象把两个端连到一起了, 这两个端里头一个是父端的连接, 另一个是子端的连接, 那这两个地方都具备以下这种情况:通过管道发送过来的数据, 会在另一端立即可见到。有了这个管道, 我们两个进程之间进行通信就变得容易了。管道的关键特点就是:数据会从发送方进程的内存空间复制到管道的缓冲区里面去, 接着再传递到接收方进程的内存空间里头, 所以说呢, 哪怕是这两个进程之间没有直接共享同一片内存区域, 它们依然能够开展相互之间的通信操作。第 5 级使用 Queue 实现进程间数据共享Pipe 仅仅支持在两个进程之间进行通信, 也就是说其使用范围仅限于管道的两个端点。当面临更为复杂的场景时, 使用 Queue模块中的 Queue 是一个明智的决策, 因为它是基于进程安全的 FIFO先进先出数据结构而构建的, 能够同时应对多个生产者以及多个消费者的请求, 其中生产者是负责向队列内部放入数据的进程, 消费者则是负责从队列内部获取数据的进程。上述这段代码实际上实现了一个用于多进程通信的场景之中, 在这种场景下存在一个队列。具体的运行模式表现为其中某一个进程负责把数据写入到这个队列内部去, 而与此同时, 另一个进程则从该队列中将数据读取出来。对于涉及多进程的这样的队列而言, 有两个操作是非常关键且核心的内容。Queue 和普通基于线程的队列, 也就是 queue.Queue 这个模块里面的东西, 是有区别的, queue.Queue 这个模块限制只能用在单个进程之内的那些线程进行通讯的时候, 不可以用来做进程和进程之间的通讯, 如果要在多进程的环境之下使用 queue.Queue, 数据就没法被其他那些进程去访问了, 因为进程和进程之间是不共享内存空间的, 。由于每一个进程都会有属于自己的那块内存空间, 那么就会导致 queue.Queue 这个情况变得没有效果。在第 6 级中, 我们通过使用列表, 来实现栈式进程间数据共享功能。就像上面写的那段代码显示的那样, .list 这个方法提供了一种操作方式, 能够让不同的进程去使用那些看起来像是栈结构一样的对象。而且它并不是把内存直接放到多个进程之间进行共享的意思。第 7 级了解如何在进程间共享内存在通常的情况下, 进程和进程之间是不会去共享内存的。所以每一个进程都必须要在自己独有的内存空间里面去运行。这样做的主要目的就是为了实现隔离的效果并且保证安全性。这在多进程编程的过程里绝对是一个特别关键的核心概念。不过, 拷贝那些规模巨大的数据的耗费情况, 往往并不处于我们能够轻易承受的范围之内。系统已经供给了一些明确的办法, 以便让内存能够在不同的进程之间进行共享操作, 而这些办法的使用时机是需要被严格把控的。以下列举出的一些状况作为示例。然而, 在应用这类机制的进程里, 我们务必保持警惕, 因为关于共享内存的操作有可能导致并发访问时出现互斥失控的情况, 比如像下面这段代码所体现的那样, 它调用了这种方法以便让四个进程共同使用同一个整数型变量。从3.2这个版本开始, .Value的内部就已经采用了同步机制了, 这样的做法是为了保证对共享数据的处理能够满足线程安全的条件。就算没有进行手动加锁的操作, 它也会确保相关的操作具有原子性的特点, 这样一来就能够避免竞争条件的发生问题了。在第8级的时候, 需要采用锁的方式来解决多进程在使用共享内存的过程中可能出现的竞争条件这一情况。模块给我们提供了 Lock 这个对象, 它能够帮助我们避免竞争条件。当多个进程一起访问共享内存里面的数据时, 如果没使用适当的同步机制, 很有可能会发生竞争状况。为了解决这个问题, 我们能用 Lock。它能确保在任何时候, 只能有一个进程去访问共享资源, 以此保证数据不会出现不一致的现象。Lock, 是一种基础的同步工具, 通常都是用来和共享数据一块儿配合使用的, 它的作用是让一个进程去访问共享资源的时候, 其他进程都被挡在外面进不了临界区, 也就是说进不了那个专门用来访问共享资源的代码段落部分, 这样一来同步的效果就达到了。在这个示例里面, Lock 这个东西用来保证说, 每次只允许一个进程去访问那个地方。通过使用 with lock: 这种说法来写的语句, 我们保证了每个进程在它想要修改共享变量的之前去把锁给获取住, 并且在它做完事情之后把锁给释放掉, 这样就避免了因为多个进程同时去修改共享数据而导致的那个叫做竞争条件的麻烦事儿发生。里面的多进程编程是一种高级的技术, 它能够明显提高应用程序的运行速度, 尤其是在处理那些非常消耗CPU资源的工作的时候。这八个层面的解释还有配套的示例, 都是为了展示多进程编程的基本概念, 从而帮助你建立起扎实的基础知识, 让你能够顺利地开始进入多进程开发的领域里面去进行学习和实践。