并发与并行

并发和并行都用于多线程程序,但人们对它们之间的相似性和差异性有很多困惑。在这方面最大的问题是:并发是否是并行?虽然​​这两个术语看起来非常相似,但上述问题的答案是否定的,并发和并行并不相同。现在,如果它们不一样,那么它们之间的基本区别是什么?

简单来说,并发涉及管理从不同线程对共享状态的访问,而并行涉及利用多个 CPU 或其核心来提高硬件性能。

并发详解

并发是指两个任务在执行中重叠。这可能是一个应用程序同时执行多个任务的情况。我们可以用图表来理解它;多个任务同时取得进展,如下所示 −

Concurrency

并发级别

在本节中,我们将讨论编程中三个重要的并发级别 −

低级并发

在此级别的并发中,明确使用了原子操作。我们不能使用这种并发来构建应用程序,因为它很容易出错并且很难调试。甚至 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://www.tutorialspoint.com')

pageHtml = req.read()

te = time.time()

print("Page Fetching Time : {} Seconds".format (te-ts))

执行上述脚本后,我们可以得到页面获取时间,如下所示。

输出

Page Fetching Time: 1.0991398811340332 Seconds

我们可以看到,获取页面的时间超过一秒。现在,如果我们要获取数千个不同的网页,您可以了解我们的网络将花费多少时间。

什么是并行性?

并行性可以定义为将任务拆分为可以同时处理的子任务的艺术。它与上面讨论的并发相反,并发中两个或多个事件同时发生。我们可以用图表来理解它;一个任务被分解成多个可以并行处理的子任务,如下所示 −

Parallelism

要进一步了解并发和并行之间的区别,请考虑以下几点 −

并发但不并行

应用程序可以并发但不能并行意味着它同时处理多个任务,但任务不会分解为子任务。

并行但不并发

应用程序可以并行但不能并发意味着它一次只能处理一个任务,分解为子任务的任务可以并行处理。

既不并行也不并发

应用程序既不能并行也不能并发。这意味着它一次只处理一个任务,并且任务永远不会被分解成子任务。

并行和并发

应用程序可以同时并行和并发,这意味着它一次可以处理多个任务,并且任务被分解成子任务以并行执行。

并行的必要性

我们可以通过将子任务分配到单个 CPU 的不同核心或网络中连接的多台计算机之间来实现并行。

考虑以下要点以了解为什么需要实现并行 −

高效代码执行

借助并行性,我们可以高效运行代码。这将节省我们的时间,因为相同的代码部分并行运行。

比顺序计算更快

顺序计算受到物理和实际因素的限制,因此不可能获得更快的计算结果。另一方面,并​​行计算解决了这个问题,并为我们提供比顺序计算更快的计算结果。

执行时间更短

并行处理减少了程序代码的执行时间。

如果我们谈论并行性的现实示例,我们计算机的显卡就是突出并行处理的真正威力的例子,因为它有数百个独立工作并可以同时执行的单独处理核心。由于这个原因,我们能够运行高端应用程序和游戏。

了解用于实现的处理器

我们知道并发、并行以及它们之间的区别,但对于要实现它们的系统又了解多少呢?了解我们要实现的系统是非常必要的,因为它使我们在设计软件时能够做出明智的决定。我们有以下两种处理器 −

单核处理器

单核处理器能够在任何给定时间执行一个线程。这些处理器使用上下文切换在特定时间存储线程的所有必要信息,然后在稍后恢复该信息。上下文切换机制帮助我们在给定的秒内处理多个线程,看起来好像系统正在处理多个任务。

单核处理器具有许多优势。这些处理器所需的功率较低,并且多核之间没有复杂的通信协议。另一方面,单核处理器的速度有限,不适合较大的应用程序。

多核处理器

多核处理器具有多个独立的处理单元,也称为核心

此类处理器不需要上下文切换机制,因为每个核心都包含执行存储指令序列所需的一切。

获取-解码-执行周期

多核处理器的核心遵循一个执行周期。此周期称为获取-解码-执行周期。它涉及以下步骤 −

获取

这是周期的第一步,涉及从程序内存中获取指令。

解码

最近获取的指令将转换为一系列信号,这些信号将触发 CPU 的其他部分。

执行

这是执行获取和解码的指令的最后一步。执行结果将存储在 CPU 寄存器中。

这里的一个优点是多核处理器的执行速度比单核处理器更快。它适用于较大的应用程序。另一方面,多个核心之间的复杂通信协议是一个问题。多核比单核处理器需要更多的功率。