
并发并发concurrency和其表现形式之一—并行处理parallel processing—是软件工程领域最广泛的话题之一。这本书中的大部分章节也涵盖广阔的领域几乎所有的章节的主题都很大足可以写一本单独的书。然而并发的主题本身就很大它可能需要几十个篇幅来讲即使这样我们仍然无法讨论完它所有重要的方面和模型。这就是为什么我不会试图欺骗你从一开始的状态我们几乎不会碰到这个话题的表面。本章的目的是说明为什么在应用程序中需要并发什么时候使用它以及在 Python 中你可以使用的最重要的并发模型。• 多线程multithreading。• 多进程multiprocessing。• 异步编程asynchronous programming。我们还将讨论一些语言特性内置模块和第三方包你可以使用它们在代码中实现这些模型。但我们不会详细地介绍它们。本章内容将作为你进一步研究和阅读的切入点。本章主要指出一些基本理念并帮助决定是否真的需要并发如果是哪种方法将最适合你的需求。在我们回答“为什么需要并发”这一问题之前我们需要问什么是并发第二个问题的答案可能是令一些人感到意外这些人曾经认为那是并行处理的同义词。但并发与并行是不同的。并发不是应用程序实现的问题而只是程序算法或问题的属性。并行只是并发问题的可能的方法之一。Leslie Lamport 在 1976 年的 Time, Clocks, and the Ordering of Events in Distributed Systems一文中说道如果两个事件互不影响则两个事件是并发的。通过推断程序、算法或问题中的事件我们可以说如果它们可以被完全或部分分解为顺序无关的组件单位则这些事件是并发的。可以彼此独立地处理这些单元并且处理的顺序不会影响最终的结果。这意味可以同时地或并行地处理它们。如果我们以这种方法处理信息那么我们实际上是直面并行处理但这还不是强制性的。所以一旦我们知道什么是并发那是时候解释如何处理并发。当问题是并发的你就有机会以一种特殊的更有效的方式来处理它。我们经常习惯于通过传统的方式来处理问题这种方式是执行一序列的步骤。这是我们大多数人思考和处理信息的方式即使用同步算法一步一步地做一件事。但是这种处理信息的方式不太适合解决大规模问题或者当你需要同时满足多个用户或软件代理的需求时• 处理作业的时间受单个处理单元单机、CPU 内核等的性能的限制。• 在程序完成对上一个输入的处理之前你不能接受和处理新的输入。因此一般来说对于以下情况并发地处理并发问题是最佳方法• 扩展问题很重要并且在可接受的时间或可用资源范围内处理它们的唯一方法是将执行分配到可并行处理工作的多个处理单元上。• 你的应用程序需要保持响应接受新输入即使它尚未完成处理旧的输入。这涵盖了可以使用并发处理问题的大多数情况。第一组问题肯定需要并行处理解决方案因此通常使用多线程和多处理模型来解决。第二组不一定需要并行处理因此真实的解决方案实际上取决于问题的细节。请注意此组还涵盖一种情况就是应用程序需要独立地为多个客户端用户或软件代理提供服务而无需等待其他客户端被成功处理。另一件值得一提的是前两组不是互相排斥。通常你需要维护应用程序响应性同时你无法在单个处理单元上处理输入。这就是为什么不同的并且看似可替代或冲突的并发方法可能经常同时使用的原因。这在 Web 服务器的开发中尤其常见这里可能需要使用异步事件循环或结合多个进程的线程以便利用所有可用资源并且在高负载下维持低延迟。多线程通常开发人员认为线程是一个复杂的主题。虽然这个说法是完全正确的然而 Python提供了一些高级类和函数通过它们可以轻松地使用线程。CPython 的线程实现中带有一些麻烦的细节使得它们没其他语言那么实用。对于你可能想要解决的一些特定的问题它们仍然是完全正确的但是它们不和 C 或 Java 一样解决同样多的问题。在本节中我们将讨论 CPython中多线程的局限性以及使用 Python 线程作为可行解决方案时的一些常见并发问题。什么是多线程线程是执行线程的缩写。程序员可以将他或她的工作拆分到线程中这些线程同时运行并共享同一内存上下文。除非你的代码依赖第三方资源否则多线程不会在单核处理器上加速甚至会增加线程管理的开销。多线程得益于多处理器或多核机器将在每个 CPU核上并行化每个线程执行从而使程序更快。请注意这是一个通用规则应该适用于大多数编程语言。在 Python 中多核 CPU 的多线程的性能优势有一些限制我们稍后将讨论。为了简单起见让我们假设这个语句是真的。事实上线程之间共享同样的上下文这意味着你必须保护数据避免并发访问这些数据。如果两个线程更新相同的没有任何保护的数据则会发生竞态条件。这被称为竞争冒险race hazard这里可能发生意外的结果因为每个线程运行的代码对数据的状态做出了错误的假设。锁机制有助于保护数据在多线程编程中总是要确保线程以安全的方式访问资源。这可能相当困难多线程编程通常导致难以调试的 bug因为很难重现这些 bug。最糟糕的问题是由于糟糕的代码设计两个线程锁定一个资源并尝试获取另一个线程锁定的资源。它们将永远彼此等待。这被称为死锁deadlock并且很难调试。可重入锁Reentrantlocks有助于这种情况它通过确保线程在尝试两次锁定资源时不会被锁定。然而当线程使用构建线程的工具处理孤立的需求时它们可能会提高程序的速度。在系统内核级别通常支持多线程。当机器具有带有单个核的单个处理器时系统使用时间分片timeslicing机制。这里CPU 可以很快地从一个线程切换到另一个线程造成了线程同时运行的错觉。这也是在处理级别完成的。没有多个处理单元的并行显然是虚拟的并且在这样的硬件上运行多个线程不会改善性能。无论如何使用线程实现代码有时仍然有用即使它必须在单个核上执行我们会在后面看到一个可能的使用案例。当你的执行环境具有多个处理器或多个 CPU 核心进行处理时一切都会改变。即使使用时间分片进程和线程也会分布在 CPU 之间提供了更快运行程序的能力。