Python - 并发性 vs 并行性
并发性和并行性都用于多线程程序,但对于它们之间的相似性和差异存在很多混淆。这方面的一个大问题是:并发并行性与否?尽管这两个术语看起来非常相似,但上述问题的答案是否定的,并发性和并行性并不相同。现在,如果它们不相同,那么它们之间的基本区别是什么?
简单来说,并发处理管理来自不同线程对共享状态的访问,另一方面,并行处理利用多个 CPU 或其内核来提高硬件的性能。
并发详细信息
并发是指两个任务在执行中重叠。这可能是应用程序同时处理多个任务的情况。我们可以用图解来理解它;多个任务同时进行,如下所示 -
并发级别
在本节中,我们将讨论编程方面的三个重要并发级别 -
低级并发
在此级别的并发中,显式使用原子操作。我们不能使用这种并发来构建应用程序,因为它很容易出错且难以调试。甚至 Python 也不支持这种并发。
中级并发
在这种并发中,不使用显式原子操作。它使用显式锁。Python 和其他编程语言支持这种并发。大多数应用程序程序员都使用这种并发性。
高级别并发
在此并发中,既不使用显式原子操作,也不使用显式锁。Python 有 concurrent.futures 模块来支持这种并发。
并发系统的属性
要使程序或并发系统正确,它必须满足某些属性。与系统终止相关的属性如下 -
正确性属性
correctness 属性意味着程序或系统必须提供所需的正确答案。为简单起见,我们可以说系统必须正确地将启动程序状态映射到最终状态。
安全特性
safety 属性意味着程序或系统必须保持 “good” 或 “safe” 状态,并且永远不会做任何 “坏” 的事情。
活动性属性
此属性意味着程序或系统必须 “取得进展”,并且它将达到某种理想的状态。
并发系统的 Actor
这是并发系统的一个常见属性,其中可以有多个进程和线程,它们同时运行以推进自己的任务。这些进程和线程称为并发系统的参与者。
并发系统的资源
参与者必须利用内存、磁盘、打印机等资源来执行他们的任务。
特定的规则集
每个并发系统都必须拥有一组规则来定义参与者要执行的任务类型以及每个任务的时间安排。任务可以是获取锁、内存共享、修改状态等。
并发系统的障碍
在实现并发系统时,程序员必须考虑以下两个重要问题,这可能是并发系统的障碍 -
数据共享
实现并发系统时的一个重要问题是在多个线程或进程之间共享数据。实际上,程序员必须确保锁保护共享数据,以便对共享数据的所有访问都是序列化的,并且一次只有一个线程或进程可以访问共享数据。如果多个线程或进程都试图访问相同的共享数据,则除了至少一个线程或进程之外,并非所有线程或进程都会被阻止并保持空闲状态。换句话说,我们可以说,当锁生效时,我们一次只能使用一个进程或线程。可以有一些简单的解决方案来消除上述障碍 -
数据共享限制
最简单的解决方案是不共享任何可变数据。在这种情况下,我们不需要使用显式锁定,并且将解决由于互数据而导致的并发障碍。
数据结构辅助
很多时候,并发进程需要同时访问相同的数据。与使用显式锁相比,另一种解决方案是使用支持并发访问的数据结构。例如,我们可以使用 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://www.qikepu.com')
pageHtml = req.read()
te = time.time()
print("Page Fetching Time : {} Seconds".format (te-ts))
执行完上面的脚本后,我们可以得到获取到页面时间,如下所示。
输出
我们可以看到,获取页面的时间超过 1 秒。现在,如果我们想获取数千个不同的网页,您可以了解我们的网络需要多少时间。
什么是并行度?
并行性可以定义为将任务拆分为可以同时处理的子任务的艺术。它与并发相反,如上所述,在并发中,两个或多个事件同时发生。我们可以用图解来理解它;任务被分解为多个可以并行处理的子任务,如下所示 -
要更深入地了解并发和并行之间的区别,请考虑以下几点 -
并发但不并行
应用程序可以是并发的,但不能是并行的,这意味着它同时处理多个任务,但这些任务不会分解为子任务。
并行但非并发
应用程序可以是并行的,但不能是并发的,这意味着它一次只能处理一个任务,并且分解为子任务的任务可以并行处理。
既不是并行的,也不是并发的
应用程序既可以是并行的,也可以是并发的。这意味着它一次只处理一个任务,并且该任务永远不会分解为子任务。
并行和并发
应用程序可以是并行的,也可以是并发的,这意味着它一次都处理多个任务,并且任务被分解为子任务以并行执行它们。
并行的必要性
我们可以通过在单个 CPU 的不同内核之间或在网络内连接的多台计算机之间分配子任务来实现并行性。
请考虑以下要点,以了解为什么需要实现并行性 -
高效的代码执行
在并行性的帮助下,我们可以有效地运行我们的代码。这将节省我们的时间,因为相同的代码分部分是并行运行的。
比顺序计算更快
顺序计算受物理和实际因素的限制,因此无法获得更快的计算结果。另一方面,这个问题通过并行计算得到了解决,它给了我们比顺序计算更快的计算结果。
更少的执行时间
并行处理减少了程序代码的执行时间。
如果我们谈论现实生活中的并行性示例,我们计算机的显卡就是突出并行处理真正力量的例子,因为它有数百个独立工作并且可以同时执行的单独处理内核。由于这个原因,我们也能够运行高端应用程序和游戏。
了解用于实现的处理器
我们知道并发性、并行性以及它们之间的区别,但要实现它的系统呢?了解我们将要实施的系统是非常必要的,因为它使我们在设计软件时能够做出明智的决定。我们有以下两种处理器 -
单核处理器
单核处理器能够在任何给定时间执行一个线程。这些处理器使用上下文切换来存储线程在特定时间的所有必要信息,然后在以后恢复信息。上下文切换机制帮助我们在给定的一秒内在多个线程上取得进展,看起来系统正在处理多个事情。
单核处理器具有许多优点。这些处理器需要更少的功率,并且多个内核之间没有复杂的通信协议。另一方面,单核处理器的速度受到限制,不适合较大的应用程序。
多核处理器
多核处理器具有多个独立的处理单元,也称为内核。
这样的处理器不需要上下文切换机制,因为每个内核都包含执行一系列存储指令所需的一切。
获取-解码-执行循环
多核处理器的内核遵循一个执行周期。此循环称为 Fetch-Decode-Execute 循环。它涉及以下步骤 -
获取
这是 cycle 的第一步,它涉及从 program memory 中获取 instructions。
解码
最近获取的指令将被转换为一系列信号,这些信号将触发 CPU 的其他部分。
执行
这是执行 fetched 和 decoded 指令的最后一步。执行结果将存储在 CPU 寄存器中。
这里的一个优点是多核处理器的执行速度比单核处理器快。它适用于较大的应用。另一方面,多个内核之间的复杂通信协议是一个问题。多核处理器比单核处理器需要更多的功率。