并发与并行
并发和并行都用于与多线程程序相关联,但它们之间的相似性和差异却存在很多混淆。这方面的一个大问题是:并发是否就是并行?虽然这两个术语看起来非常相似,但上述问题的答案是否定的,并发和并行不是一回事。现在,如果它们不是一回事,那么它们之间最根本的区别是什么呢?
简单来说,并发处理的是从不同线程管理对共享状态的访问,而并行处理的是利用多个 CPU 或其核心来提高硬件性能。
并发详解
并发是指两个任务在执行上重叠。它可能是一种情况,应用程序在同一时间处理多个任务。我们可以通过图表来理解它;多个任务同时取得进展,如下所示:
并发级别
在本节中,我们将讨论编程方面三个重要的并发级别:
低级并发
在这个并发级别中,显式地使用了原子操作。我们不能使用这种并发来构建应用程序,因为它很容易出错并且难以调试。即使 Python 也不支持这种并发。
中级并发
在这种并发中,没有使用显式的原子操作。它使用显式锁。Python 和其他编程语言支持这种并发。大多数应用程序程序员使用这种并发。
高级并发
在这种并发中,既不使用显式的原子操作也不使用显式的锁。Python 拥有concurrent.futures模块来支持这种并发。
并发系统的特性
为了使程序或并发系统正确,它必须满足某些属性。与系统终止相关的属性如下:
正确性属性
正确性属性意味着程序或系统必须提供所需的正确答案。为了简单起见,我们可以说系统必须正确地将起始程序状态映射到最终状态。
安全性属性
安全性属性意味着程序或系统必须保持在“良好”或“安全”状态,并且永远不会做任何“坏事”。
活跃性属性
此属性意味着程序或系统必须“取得进展”,并且它将到达某个期望的状态。
并发系统的参与者
这是并发系统的一个常见属性,其中可以有多个进程和线程,它们同时运行以在其自己的任务上取得进展。这些进程和线程称为并发系统的参与者。
并发系统的资源
参与者必须利用内存、磁盘、打印机等资源来执行其任务。
某些规则集
每个并发系统都必须具有一套规则来定义参与者要执行的任务类型和每个任务的时机。任务可以是获取锁、共享内存、修改状态等。
并发系统的障碍
在实现并发系统时,程序员必须考虑以下两个重要问题,它们可能是并发系统的障碍:数据共享
在实现并发系统时,一个重要问题是在多个线程或进程之间共享数据。实际上,程序员必须确保锁保护共享数据,以便对其的所有访问都进行序列化,并且一次只有一个线程或进程可以访问共享数据。如果多个线程或进程都试图访问相同的共享数据,那么并非所有线程或进程都能访问,至少其中一个会被阻塞并保持空闲状态。换句话说,当锁生效时,我们一次只能使用一个进程或线程。有一些简单的解决方案可以消除上述障碍:
数据共享限制
最简单的解决方案是不共享任何可变数据。在这种情况下,我们不需要使用显式锁定,并且由于互斥数据而导致的并发障碍将得到解决。
数据结构辅助
很多时候,并发进程需要同时访问相同的数据。除了使用显式锁之外,另一个解决方案是使用支持并发访问的数据结构。例如,我们可以使用queue模块,它提供线程安全的队列。我们还可以使用multiprocessing.JoinableQueue类进行基于多处理的并发。
不可变数据传输
有时,我们使用的数据结构(例如并发队列)不合适,那么我们可以传递不可变数据而无需锁定它。
可变数据传输
作为上述解决方案的延续,假设如果需要传递仅可变数据而不是不可变数据,那么我们可以传递只读的可变数据。
I/O 资源共享
在实现并发系统中的另一个重要问题是线程或进程使用 I/O 资源。当一个线程或进程长时间使用 I/O 而其他线程或进程处于空闲状态时,就会出现问题。在处理 I/O 密集型应用程序时,我们可以看到这种障碍。它可以通过一个例子来理解,例如从 Web 浏览器请求页面。这是一个重量级的应用程序。在这里,如果数据请求速率低于数据使用速率,那么我们的并发系统中就会存在 I/O 障碍。
以下 Python 脚本用于请求网页并获取网络获取请求页面的时间:
import urllib.request import time ts = time.time() req = urllib.request.urlopen('https://tutorialspoint.com') pageHtml = req.read() te = time.time() print("Page Fetching Time : {} Seconds".format (te-ts))
执行上述脚本后,我们可以获得如下所示的页面获取时间。
输出
Page Fetching Time: 1.0991398811340332 Seconds
我们可以看到,获取页面的时间超过一秒。现在如果我们要获取数千个不同的网页,您可以理解我们的网络将花费多长时间。
什么是并行?
并行可以定义为将任务分解成可以同时处理的子任务的艺术。它与上面讨论的并发相反,并发是指两个或多个事件同时发生。我们可以通过图表来理解它;一个任务被分解成许多可以并行处理的子任务,如下所示:
为了更好地理解并发和并行之间的区别,请考虑以下几点:
并发但不并行
应用程序可以是并发但不是并行的,这意味着它同时处理多个任务,但这些任务没有被分解成子任务。
并行但不并发
应用程序可以是并行但不是并发的,这意味着它一次只处理一个任务,并且可以并行处理将任务分解成的子任务。
既不并行也不并发
应用程序可以既不并行也不并发。这意味着它一次只处理一个任务,并且该任务从未被分解成子任务。
既并行又并发
应用程序可以既并行又并发,这意味着它既可以同时处理多个任务,又可以将任务分解成子任务以并行执行它们。
并行的必要性
我们可以通过将子任务分布在单个 CPU 的不同核心之间或网络中连接的多台计算机之间来实现并行。
请考虑以下要点以了解为什么需要实现并行:
高效的代码执行
借助并行,我们可以高效地运行代码。它将节省我们的时间,因为代码的各个部分将并行运行。
比顺序计算快
顺序计算受物理和实际因素的限制,因此无法获得更快的计算结果。另一方面,并行计算解决了这个问题,并且与顺序计算相比,它可以提供更快的计算结果。
执行时间更短
并行处理减少了程序代码的执行时间。
如果我们谈论并行的现实生活例子,我们计算机的显卡就是一个突显并行处理真正能力的例子,因为它拥有数百个独立工作的独立处理核心,并且可以同时执行。由于这个原因,我们能够运行高端应用程序和游戏。
了解处理器以进行实施
我们了解了并发、并行以及它们之间的区别,但对于要实现的系统呢?了解我们将要实现的系统非常必要,因为它使我们能够在设计软件时做出明智的决策。我们有以下两种类型的处理器:
单核处理器
单核处理器能够在任何给定时间执行一个线程。这些处理器使用上下文切换在特定时间存储线程的所有必要信息,然后稍后恢复这些信息。上下文切换机制帮助我们在给定的秒内在一系列线程上取得进展,并且看起来系统正在处理多件事。
单核处理器有很多优点。这些处理器需要更少的功耗,并且多个核心之间没有复杂的通信协议。另一方面,单核处理器的速度有限,不适合大型应用程序。
多核处理器
多核处理器具有多个独立的处理单元,也称为核心。
此类处理器不需要上下文切换机制,因为每个核心都包含执行存储指令序列所需的一切。
取指令-译码-执行周期
多核处理器的核心遵循一个执行周期。此周期称为取指令-译码-执行周期。它包含以下步骤:
取指令
这是周期的第一步,它涉及从程序内存中获取指令。
译码
最近获取的指令将被转换为一系列信号,这些信号将触发 CPU 的其他部分。
执行
这是最后一步,其中获取和解码的指令将被执行。执行的结果将存储在 CPU 寄存器中。
这里的一个优势是,多核处理器中的执行速度比单核处理器快。它适用于较大的应用程序。另一方面,多个核心之间复杂的通信协议是一个问题。多个核心比单核处理器需要更多的功率。