并发与并行


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

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

并发细节

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

并发性

并发级别

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

低级并发

在此并发级别中,显式使用Atomics操作。我们不能使用这种并发来构建应用程序,因为它很容易出错并且难以调试。即使Python也不支持这种并发。

中级并发

在这种并发中,没有使用显式的Atomics操作。它使用显式锁。Python 和其他编程语言支持这种并发。大多数应用程序程序员都使用这种并发性。

高级并发

在这种并发中,既不使用显式Atomics操作,也不使用显式锁。Python有concurrent.futures模块来支持这种并发。

并发系统的属性

为了使程序或并发系统正确,它必须满足一些属性。与系统终止相关的属性如下 -

正确性属性

正确性属性意味着程序或系统必须提供所需的正确答案。为了简单起见,我们可以说系统必须正确地将起始程序状态映射到最终状态。

安全性能

安全属性意味着程序或系统必须保持在“良好”“安全”的状态,并且永远不会做任何“坏”的事情。

活性特性

这个属性意味着一个程序或系统必须“取得进展”并且它将达到某种理想的状态。

并发系统的参与者

这是并发系统的一个常见属性,其中可以有多个进程和线程,它们同时运行以在各自的任务上取得进展。这些进程和线程称为并发系统的参与者。

并发系统资源

参与者必须利用内存、磁盘、打印机等资源来执行他们的任务。

一定的规则集

每个并发系统都必须拥有一组规则来定义参与者要执行的任务类型以及每个任务的时间安排。这些任务可以是获取锁、共享内存、修改状态等。

并发系统的障碍

在实现并发系统时,程序员必须考虑以下两个重要问题,这可能是并发系统的障碍 -

数据共享

实现并发系统时的一个重要问题是多个线程或进程之间的数据共享。实际上,程序员必须确保锁保护共享数据,以便对共享数据的所有访问都是串行的,并且一次只有一个线程或进程可以访问共享数据。万一,当多个线程或进程都试图访问相同的共享数据时,不是所有线程或进程都会被阻塞,而是至少其中一个会被阻塞并保持空闲状态。换句话说,我们可以说,当锁有效时,我们一次只能使用一个进程或线程。可以有一些简单的解决方案来消除上述障碍 -

数据共享限制

最简单的解决方案是不共享任何可变数据。在这种情况下,我们不需要使用显式锁定,并且由于相互数据而导致的并发障碍也将得到解决。

数据结构协助

很多时候并发进程需要同时访问相同的数据。除了使用显式锁之外,另一种解决方案是使用支持并发访问的数据结构。例如,我们可以使用队列模块,它提供线程安全的队列。我们还可以使用multiprocessing.JoinableQueue类来实现基于多处理的并发。

不可变的数据传输

有时,我们使用的数据结构(例如并发队列)不适合,那么我们可以传递不可变数据而不锁定它。

可变数据传输

延续上述解决方案,假设如果只需要传递可变数据,而不是不可变数据,那么我们可以传递只读的可变数据。

I/O资源共享

实现并发系统的另一个重要问题是线程或进程对 I/O 资源的使用。当一个线程或进程长时间使用 I/O 而其他线程或进程处于空闲状态时,就会出现问题。在处理 I/O 密集型应用程序时,我们可以看到这种障碍。可以通过一个例子来理解,即从网络浏览器请求页面。这是一个繁重的应用程序。在这里,如果请求数据的速率低于消耗数据的速率,那么我们的并发系统中就会出现 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

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

什么是并行性?

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

并行性

要更多地了解并发和并行之间的区别,请考虑以下几点 -

并发但非并行

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

并行但非并发

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

既不是并行也不是并发

应用程序既不能是并行的,也不能是并发的。这意味着它一次仅处理一项任务,并且该任务永远不会分解为子任务。

并行和并发

应用程序可以是并行的,也可以是并发的,这意味着它可以同时处理多个任务,并且该任务被分解为子任务以并行执行它们。

并行的必要性

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

考虑以下要点来理解为什么需要实现并行性 -

高效的代码执行

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

比顺序计算更快

顺序计算受到物理和实际因素的限制,无法获得更快的计算结果。另一方面,这个问题通过并行计算得到了解决,并且给我们带来了比顺序计算更快的计算结果。

更少的执行时间

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

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

了解实施处理器

我们知道并发、并行性以及它们之间的区别,但是我们知道要在其上实现它的系统吗?了解我们将要实施的系统是非常有必要的,因为它使我们有利于在设计软件时做出明智的决策。我们有以下两种处理器 -

单核处理器

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

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

多核处理器

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

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

获取-解码-执行周期

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

拿来

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

解码

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

执行

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

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