Python 高手编程系列一千零九十九:为什么需要并发

并发
并发(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),并且很难调试。可重入锁(Reentrant
locks)有助于这种情况,它通过确保线程在尝试两次锁定资源时不会被锁定。
然而,当线程使用构建线程的工具处理孤立的需求时,它们可能会提高程序的速度。
在系统内核级别通常支持多线程。当机器具有带有单个核的单个处理器时,系统使用
时间分片(timeslicing)机制。这里,CPU 可以很快地从一个线程切换到另一个线程,造
成了线程同时运行的错觉。这也是在处理级别完成的。没有多个处理单元的并行显然
是虚拟的,并且在这样的硬件上运行多个线程不会改善性能。无论如何,使用线程实现代
码有时仍然有用,即使它必须在单个核上执行,我们会在后面看到一个可能的使用案例。
当你的执行环境具有多个处理器或多个 CPU 核心进行处理时,一切都会改变。即使使
用时间分片,进程和线程也会分布在 CPU 之间,提供了更快运行程序的能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值