Python并行编程1并行编程简介(上)高频面试题:GIL进程线程协程
|
1 并行编程简介
首先,我们将讨论允许在新计算机上并行执行的硬件组件,如 CPU 和内核,然后讨论操作系统中真正推动并行的实体:进程和线程。随后,将详细说明并行编程模型,介绍并发性、同步性和异步性等基本概念。
介绍完这些一般概念后,我们将讨论全局解释器锁(GIL)及其带来的问题,从而了解 Python 在这方面的特殊性,尤其是在线程方面。我们将提到标准的 Python 库模块,如线程和多处理,我们将在接下来的章节中更深入地介绍这些模块。最后,我们将通过讨论并行程序的评估方法(如加速和缩放)来结束本章,并讨论并行编程可能带来的问题(竞赛条件、死锁等)。
在本章结束时,你将理解并行编程背后的所有基本概念和术语。您将在脑海中构建一个总体方案,其中将包含并行执行的所有主角,以及他们是如何实现并行执行的。然后,您就可以着手处理后续章节中涉及的编程实践部分了。
内容:
- CPU 和内核
- 进程和线程
- 并行和并发编程
- 使用 Python 的 GIL 和线程
- 加速和扩展
1.1 并行编程
新硬件技术的出现让我们有机会在电脑上同时运行多个程序。事实上,我们的电脑,即使是最简单的电脑,都有一个多核系统,允许程序并行运行。那为什么不利用这种架构呢?
你经常会发现自己正在开发一个 Python 程序来执行一系列操作。在科学领域,经常需要实现一系列算法来进行非常费力的计算。但在工作结束后,在计算机上运行程序时,你会失望地发现,它并没有你希望的那么快,而且随着处理问题的规模增大,执行时间也变得过长。但这不仅仅是速度问题。如今,我们需要处理的数据量越来越大,与之相关的计算也越来越多,程序需要的内存资源也越来越大,而我们的计算机尽管功能强大,却无法处理这些数据。
并行编程可以同时执行一个程序的部分代码,从而大大提高性能。因此,并行编程意味着缩短程序的执行时间,更有效地利用资源,并能执行以前无法完成的更复杂的操作。
1.2 计算机和并行技术的发展
并行概念是随着计算机内部硬件的发展而逐步形成的。在 20 世纪 80 年代之前,计算机的功能非常有限:它们以严格的顺序方式,一条指令接一条指令地运行一个程序。显然,在这样的技术环境中,并行的概念甚至连想都不敢想。
随着英特尔 80386 处理器的问世,计算机有了中断一个程序的执行以执行另一个程序的可能性。因此,抢占式编程和时间分割等概念应运而生。这一技术进步带来了伪并行效果,因为用户可以看到多个程序同时运行。在随后的英特尔 80486 处理器中,通过引入基于将程序细分为子任务的流水线系统,情况得到了进一步改善。这些子任务在不同程序之间交替独立执行。此外,内部架构首次实现了将多个不同指令(甚至来自不同程序的指令)组合在一起,并同时(但不是同时)执行。这就是并发编程的真正发展。不同子任务的指令部分都已完成,以便尽快执行:
这种情况持续了十多年,功能越来越强大的处理器型号相继问世,工作频率比以前更高。但由于一系列问题和物理限制,这种情况很快就陷入了危机。提高执行频率意味着同时增加发热量和随之而来的能耗。显然,频率的提升很快就会达到极限。
就这样,处理器实现了创新的飞跃,在系统中引入了内核。这些内核也被称为逻辑处理器,可以在单个 CPU 中模拟多个处理器的存在,从而形成多核 CPU。实际上,多处理器计算机可以同时并行执行不同程序的指令。因此,在本世纪初,并行编程技术应运而生,为开发人员同时执行同一程序的不同部分提供了可能。
1.3 CPU、内核、线程和进程
Windows 中的任务管理器可以实时监控各种资源的消耗情况,如 CPU、内存和 Wi-Fi 网络:
此外,任务管理器还显示了各种信息,如进程数和当前运行的线程数。右侧则列出了我们正在运行的系统的一些特征,如内核数。
如果你使用的是 Ubuntu 等 Linux 系统,则可使用top:
Linux 系统更灵活、更强大,特别是由于有大量的 shell 命令,我们还可以监控与每个命令相关的所有线程。为此,我们将使用更具体的命令来监控进程:
-T选项用于表示将显示与进程相关的线程。
中央处理器(CPU Central Processing Unit)是计算机的真正大脑,基本上是处理代码的地方。中央处理器以周期为特征,即中央处理器在处理器上执行操作所使用的时间单位。通常,我们用每秒周期频率来表示 CPU 的功率。
CPU 内部可以有一个内核(单核 CPU)或多个内核(多核 CPU)。内核是 CPU 中的数据执行单元。每个内核可运行多个进程。进程本质上是在机器上运行的程序,并为其预留部分内存。此外,每个进程还可以反过来启动其他进程(子进程),或在其中运行一个(主线程)或多个线程。
反过来,线程也可视为在单个处理器中并发运行的子进程。与进程一样,线程也有一系列类似的机制来管理它们的同步、数据交换和执行过程中的状态转换(就绪、运行和阻塞)。
这就是我们必须牢记的总体框架,以便更好地理解机器中的进程和线程是如何运行的,从而以最佳方式建立并行编程模型。
1.4 并发和并行编程
并发和并行经常被混淆,这两个术语被互换使用的情况并不少见,但这是不正确的。这两个概念虽然密切相关,但在并行编程中却有所不同,了解其中的区别非常重要。
我们先来看看这两个概念的共同点。当我们的程序必须同时执行多个任务时,就会出现并发和并行。但这正是并发的含义。
并发意味着同时管理(而非执行)多个任务,但它们并不一定同时运行。
参考资料
1.5 Python 中用于并发和并行模型的线程和进程
在了解了并发编程和并行编程的区别之后,让我们再进一步。在许多编程语言中,通常的做法是将线程与并发、进程与并行联系起来。事实上,操作系统的这两个实体包含了并发和并行这两种不同的功能。
然而,就 Python 而言,将这些情况分为两种不同的编程模型是不错的选择。事实上,Python 中的线程并不像操作系统中的线程那样表现完美。Python 中的线程不能并发运行,因此不能并行操作。在 Python 中使用线程就像是在使用单核 CPU,尽管事实并非如此。
1.5.1 Python 线程问题:GIL
与其他编程语言不同,Python 中的线程不能在两个不同的内核上同时执行,这与 Python 解释器本身密切相关。事实上,Python 代码一直运行的解释器是在 CPython 中实现的,在实现过程中,人们发现它并不是完全线程安全的。也就是说,越多的线程试图共同访问某个对象(线程之间共享内存),往往就会由于竞赛条件现象而陷入不一致的状态。为了避免这个大问题,解释器中加入了全局解释器锁 (GIL)。因此,Python 设计者选择在一个进程中,一次只能执行一个线程,从而消除了这种实体的并行性(无多线程)。
但情况并没有那么糟糕。事实上,稍后我们将看到如何在并行编程模型中调整 Python 语言线程的这一特性。此外,许多外部库并不依赖于 GIL,因为它们是用其他语言(如 C 和 Fortran)实现的,因此可以利用使用多线程的内部机制。NumPy 正是这些库中的一个,它是 Python 数值计算的基础库。
1.5.2 取消 GIL 以实现多线程计算
Python 3.13引入了一个名为"free-threaded"的实验性功能,允许在一定条件下选择性地关闭GIL。这为未来完全解除GIL奠定了基础。 即使启用了"free-threaded"功能,也并非所有场景下都能完全发挥多核CPU的性能。一些复杂的情况可能仍受到GIL的影响。
对于CPU密集型任务,使用多进程可以绕过GIL的限制,充分利用多核CPU。
将Python代码中的性能瓶颈部分用Cython编写,可以获得接近C语言的执行效率,同时又能享受Python的便利性。
如Jython、PyPy等,它们对GIL的处理方式不同,可能更适合某些特定的应用场景。
1.5.3 Python 中的线程与进程
特点进程线程独立性独立性高,一个进程崩溃不会影响其他进程独立性较低,一个线程崩溃可能导致整个进程崩溃资源开销创建和销毁进程开销较大,资源消耗相对较高创建和销毁线程开销较小,资源消耗较低通信进程间通信相对复杂,通常需要使用IPC机制线程间通信相对简单,可以直接共享进程内存空间切换开销进程切换开销较大线程切换开销较小适用场景CPU密集型任务,独立的子任务,需要隔离的程序I/O密集型任务,需要共享数据的并发任务1.5.4 Python 中的并发与并行
在 Python 中使用线程进行并发编程,这些线程各自独立执行任务,并相互竞争执行。
进程非常适合同时执行任务,即并行执行。
1.5.5 greenlet的轻并发(Coroutine 协程)
除了线程,Python 还提供了另一种可能的选择:greenlet。从竞争的角度来看,使用greenlet和线程是等价的,因为在 Python 中,线程从来都不是并行执行的,因此使用这种编程语言,两者都能完美地进行并发编程。但创建和管理小绿点所需的资源要比线程少得多。这就是为什么在编程中使用它们被定义为轻并发的原因。因此,当你需要管理大量简单的 I/O 功能(如网络服务器中的功能)时,通常会使用greenlet。我们将在本书后面通过几个简单的例子来了解如何创建和管理 greenlet。
特点线程协程上下文切换操作系统内核调度,开销较大用户态调度,开销较小创建开销较高较低并发模型多线程协作式多任务应用场景CPU密集型任务、并行计算I/O密集型任务、异步编程1.5.6 使用 Python 进行并行编程
了解线程和进程在 Python 中的作用。我们可以深入研究与 Python 语言密切相关的并行编程。
因此,在这种语言中,并行编程完全是在进程上表现出来的。一个程序被划分为多个可并行的子任务,每个子任务分配给不同的进程。在每个子任务中,我们可以选择是同步执行还是异步执行各个步骤。
1.5.7 同步和异步编程
在本书和许多有关并行编程的在线文档中,经常会提到同步或异步这两个术语,有时也称为sync和async。在所有这些情况下,我们指的是两种不同的编程模型。
在不知不觉中,当我们在多个进程或线程之间并行或竞争地执行一个程序时,我们会自然而然地将其结构化为同步。这是因为一般来说,我们都是串行编程出身,倾向于这样思考问题。也就是说,在存在两个或多个进程(但它们可以是线程,也可以是程序中的简单函数)的情况下,一个进程继续执行,直到执行外部调用,将执行传递给另一个进程以获取服务、计算或任何其他操作。另一个进程将被执行以完成其任务,然后将服务结果返回给同时待处理的初始进程。一旦获得必要的结果,初始进程将恢复执行。
正如我们所猜测的那样,异步编程可以让我们在很多情况下发挥优势,在这些情况下,我们会浪费大量时间等待需要外部响应或较长的执行时间的操作。因此,如果想充分利用并行编程的所有潜力,就必须对这两种模式了如指掌。
至于它的实际应用,虽然对我们来说还不完全直观,但它是完全可行的。所有编程语言都有内部机制来实现它们。我们将在第 6 章 “使用 CUDA 实现 GPU 编程性能最大化 ”中深入介绍异步编程。
1.5.8 Map和reduce
并行编程中广泛使用的一种方案是 Map-Reduce,它主要基于两个阶段:
- Map
- Reduce
第一个阶段,即映射阶段,是将程序要执行的任务细分为几个部分(任务),然后将它们分配给不同的进程,由这些进程同时执行,即并行执行。通常情况下,每个进程的执行都会产生一个结果。因此,除了与并行执行严格相关的阶段外,还会有一个后续阶段,即必须将所有结果重新组合在一起的还原阶段。
1.5.9 CPU 绑定和 I/O 绑定操作
不过,在并行程序的设计阶段,必须关注各个任务,评估其中是否有一些任务需要太长的执行时间。如果出现这种情况,就会导致性能下降,因为所有其他进程都在等待完成映射阶段。事实上,要进入还原阶段,就需要每个进程得到的所有结果。
因此,在这些情况下,我们必须考虑在每个进程(任务)中执行的各种操作。这些任务可能包括读取文件或调用外部网络服务等内部操作。在这种情况下,进程必须等待外部设备的响应,因此执行时间可能无法预测。这类操作被称为 I/O 约束。而只涉及 CPU 内部计算的操作则称为 CPU 绑定。
在并行编程中,当处理子任务或 CPU 绑定操作时,使用多个进程并行执行指令可提高程序的效率。但在 I/O 绑定的情况下,我们必须采用不同的工作方式。
在这种情况下,最合适的编程方式就是并发编程,这就是线程发挥作用的地方。在进程中,我们可以创建多个线程。其中一个线程将继续处理 CPU 绑定操作,而其他线程将处理各种 I/O 绑定操作。当其中一个包含绑定 I/O 操作的线程等待数据或来自外部的响应时,其他线程将继续执行其操作。
在这种情况下,通过并发运行线程,我们可以节省执行时间:
上图除了主线程(在进程中始终存在)外,创建一个额外的线程可以单独管理(异步或同步)I/O 绑定操作,同时允许主线程继续进行数据处理。
1.5.10 并行编程中的其他注意事项
在处理并行编程时,使用线程时仍须特别注意共享数据的管理。如图 1.17 所示,事实上,进程中的线程既有自己的独立内存(其他线程无法访问),也有共享内存空间,其中的对象可供所有线程访问:
尽管在使用多线程的 Python 解释器中存在 GIL,但如果不想陷入数据不一致的问题,仍有必要锁定共享内存空间中的全局对象。但是,如果 GIL 保证在一个进程中一次只执行一个线程,这怎么可能呢?
事实上,解释器只负责 Python 的内部对象,至于我们在程序执行过程中定义和创建的对象,将不会有控制或锁,而是独立管理。我们需要管理我们创建的全局对象的锁,以确保我们不会得到意想不到的结果。
在这方面,我们很快就会看到,Python 标准库中有一些模块,除了实现进程,特别是线程外,还提供了一系列工具,让我们也能管理全局对象的锁。
1.5.11 Threading和multiprocessing 模块
在实际执行过程中,我们可以使用标准库提供的两个模块:threading和multiprocessing。这些模块在 Python 中提供了一组函数,用于与操作系统接口,用 Python 创建、执行和管理进程和线程。
注意:Python 没有专门的多线程模块,它不像进程那样被称为多线程,因为 Python 实际上不是多线程的,而是一次只能执行一个线程。
threading模块为 _thread 模块提供了一个抽象层,而 _thread 模块是一个底层模块,它提供了处理多个线程的原语。此外,它还提供了一系列工具,帮助程序员完成管理线程等并发系统的艰巨任务:锁、条件和 Semaphores。下一章 “构建多线程程序 ”将深入介绍该模块的功能和这些工具,并提供一系列示例代码,帮助你了解如何以及何时使用这些工具。
另一方面,多处理模块提供了一个有效的应用程序接口,用于实现基于进程的并行性。除了创建和管理进程外,该模块还提供了大量有助于管理程序中多个进程共存的功能。例如,队列(Queue)和管道(Pipe)是允许在不同进程间交换信息(对象)的对象,或者是简化同时管理多个进程的池。此外,该模块及其功能将在第 3 章 “使用多进程和 mpi4py 库 ”中进行广泛讨论。
来源:https://www.cnblogs.com/testing-/p/18546611
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|
|
|
发表于 2024-11-15 02:05:13
举报
回复
分享
|
|
|
|