在现代计算机科学领域,"并发"和"并行"是设计高性能和响应式应用程序的基础概念。然而,这两个术语经常被混淆使用,导致了显著的误解。本报告提供了一个全面的分析,详细阐述了这两个关键概念之间的区别。
并发是程序的结构属性,关注其如何设计以管理在重叠时间段内执行的多个任务。而并行则是执行属性,指那些任务的同时执行。本报告将定义每个术语,明确它们的核心技术差异,探索它们在流行编程语言中的实现方式,并分析它们各自的性能权衡和硬件依赖关系。
核心论点是:虽然并行需要并发,但并发并不必然需要并行;理解这种关系对于充分利用现代多核硬件的全部潜力至关重要。
这两个概念的本质区别在于程序的设计意图与实际执行方式之间的差异。并发是一个设计时的概念,而并行是一个运行时的现象。
并发是指系统能够组合独立执行的进程或任务,在重叠的时间段内运行的能力。关键在于管理和同时推进多个任务,但这并不意味着它们在精确的同一时刻执行。
厨师并发处理多个任务的类比
在一个单处理器核心的系统上,通过任务的交错执行来实现并发。操作系统或应用程序运行时快速切换任务——这个过程称为上下文切换——给用户一种"同时性"的错觉。
就像一位厨师准备多道菜的宴席:厨师可能先开始烧水煮意大利面(任务A),然后切蔬菜做沙拉(任务B),再检查一下煮水情况(任务A),最后把烤肉放进烤箱(任务C)。厨师正在并发处理多个任务,在相同的时间框架内推进所有任务,但在任何给定时刻只专注于一个具体动作。
并发本质上是一种结构概念,允许程序同时处理很多事情,即使它一次只真正工作在一个任务上。其主要目标通常是提高响应速度并高效处理I/O密集型操作,其中任务在等待外部事件(如网络响应或磁盘读取)时花费大量时间。
另一方面,并行性指的是多个任务或大问题的子任务的同时执行。这不是一种错觉,而是计算在物理时间上的真正同时运行。这本质上是一个执行概念,专注于同时做多件事以提高计算速度。
这带来了其不可协商的前提条件:没有多个处理单元(如多核CPU、多处理器系统或GPU),并行是不可能的。
回到厨师的类比,实现并行性意味着雇佣一位助手。现在,主厨可以切蔬菜(任务B),而助手厨师同时在烧意大利面(任务A)。两个任务因为有两个"处理单元"(厨师)而同时进行。
并行性的主要目标是通过将工作分配给多个核心并在并行中执行各个部分来增加吞吐量并减少完成计算密集型任务所需的总时间。
这些概念之间的关系是分层的。并行是并发的一个子集。一个程序可以在没有并行的情况下是并发的,但如果没有首先设计成并发的,就不可能是并行的。
单厨师在单核CPU上处理多个任务。程序被设计为处理多个客户端或作业,但由于只有一个核心,它只能通过交错执行一次推进一个任务。
团队厨师在厨房里使用多核CPU工作。程序被设计为处理多个任务(并发),而底层硬件的多个核心允许这些任务的并行执行。
因此,准确地说:并行意味着并发,但并发不一定意味着并行。开发人员首先设计一个并发解决方案来解决一个问题;该解决方案是否以并行方式执行取决于它运行的硬件。
除了概念定义之外,并发和并行在技术实现、硬件需求和最终目标方面存在显著差异。
并发和并行之间的理论差异体现在编程语言如何为开发者提供工具。语言模型决定了编写、管理和推理并发代码的难易程度,这些代码可能以并行方式执行。
Go编程语言从一开始就以并发为中心设计。其模型基于一种称为通信顺序进程(CSP)的范式。
Go不直接暴露重量级的操作系统级线程,而是提供goroutines。这些是由Go运行时管理的极其轻量级的线程,而不是操作系统。它们具有较小的初始堆栈大小,创建和管理成本远低于传统线程,使得拥有数十万甚至数百万个goroutine同时运行成为可能。
为了管理goroutine之间的通信,Go提倡使用channels。Channels是类型的通道,可以通过它们发送和接收值,有效地同步不同goroutine的执行。这种模型鼓励开发人员通过在独立进程之间通信数据而不是共享内存和使用锁来构建并发代码,后者是诸如竞争条件等常见错误的来源。
Go运行时包含一个复杂的调度程序,该调度程序将许多轻量级的goroutine多路复用到较少数量的操作系统线程上。默认情况下,Go配置为使用机器上的所有可用CPU核心。这意味着用goroutine编写的并发程序将在多核硬件上自动以并行方式执行,而无需开发人员付出额外的努力,无缝地将并发设计转换为并行执行。
Java对并发的方法更为传统,直接向开发者暴露操作系统级线程。
在Java中,主要通过创建Thread类的实例来实现并发。这些线程与底层操作系统线程的映射更直接,比Go的goroutine更重且资源消耗更大。创建数千个Java线程会迅速耗尽系统资源,因为内存消耗和操作系统级上下文切换的开销很大。
Java的并发模型主要基于共享内存。多个线程访问同一内存中的数据,开发人员必须使用显式同步机制(如synchronized关键字、锁和java.util.concurrent包中的线程安全数据结构)来防止数据损坏和竞争条件。虽然功能强大,但这种模型给开发者带来了正确管理状态的高负担。
多线程Java应用程序将在多核系统上以并行方式运行,因为操作系统会将不同的线程调度到不同的核心上。Fork/Join框架和并行流是后来添加到语言中的特定功能,旨在简化计算密集型工作并行化的任务。
实现哲学中的关键差异在于抽象级别。Go抽象了操作系统线程,提供了一个更高层次、更轻量级的模型(goroutines和channels),专为大规模并发而设计。Java提供了一个更低层次、更直接映射到操作系统线程的模型,需要更仔细地手动管理共享状态。
选择为并发设计还是优化为并行完全取决于手头的问题和可用的硬件,每种方法都有不同的性能特征和挑战。
并发设计的主要好处是提高响应性和资源利用率,特别是对于I/O密集型应用程序。通过允许程序在等待网络或磁盘操作时切换到另一个任务,CPU保持忙碌,应用程序对用户保持响应。
然而,并发并非免费的。上下文切换——保存一个任务的状态并加载另一个任务的状态——会产生开销。如果对纯计算任务尝试在单个核心上与其他任务并发运行,由于在它们之间切换所浪费的时间,总执行时间可能会增加。
此外,编写正确的并发代码很困难。它引入了复杂的潜在错误,如竞争条件(结果取决于操作的非确定性时序)和死锁(任务无限期地等待彼此)。
并行的明显好处是大幅减少CPU密集型问题的执行时间。通过将任务分布在四个核心上,理论上可以实现4倍的速度提升。
并发与并行的区别不仅仅是学术性的;它是现代软件工程的基石。并发是构建程序以处理多个执行流的概念框架。它是一种设计选择,旨在管理复杂性并提高响应速度。并行是这些流在多核硬件上同时执行以实现性能提升的物理执行。
并行系统本质上是并发的,但并发系统可能或可能不是并行的,这取决于它运行的硬件。高性能计算的旅程涉及首先设计一个稳健的并发架构。这种结构解锁了利用并行力量的潜力。
正如Go和Java等语言所证明的那样,开发人员的工具和抽象严重影响这种潜力的实现方式。最终,掌握这两个概念——了解何时为并发构建结构以及如何为并行优化——对于构建定义当前计算时代的可扩展、高效和响应式应用程序至关重要。
引言
核心概念解析
2.1 并发性:管理多个任务
2.2 并行性:同时执行任务
2.3 基本关系:并发使并行成为可能
无并行的并发
有并行的并发
技术差异对比
特性
并发
并行
执行方式
交错/重叠。任务在重叠的时间段内启动、运行和完成,但不一定在精确的同一时刻。通过上下文切换推进进度。
同时。两个或多个任务在精确的同一时间执行。
硬件要求
单核可行。可以通过快速任务切换在单个CPU核心上实现。
多核必需。需要具有多个处理单元(如多核CPU、GPU)的硬件。
主要目标
响应性和延迟降低。旨在防止应用程序在处理多个独立事件时冻结,常见于I/O密集型或交互式应用程序。
吞吐量和速度。通过将劳动分配给多个核心来更快地完成大型计算密集型任务。
本质
结构性。一种处理同时处理多件事的程序结构方式。
执行性。一种同时执行程序任务的方式。
适用领域
适合处理多个外部代理或事件的问题,如Web服务器处理来自多个用户的请求或桌面GUI在执行后台任务的同时响应用户输入。
适合CPU密集型且可分解为离散、独立子问题的问题,如渲染3D场景、训练机器学习模型或执行复杂科学计算。
编程语言实现
4.1 Go:基于并发优先的方法
Goroutines
Channels
从并发到并行
4.2 Java:传统的基于共享内存的线程模型
Threads
共享内存和锁
从并发到并行
性能与权衡
5.1 并发的成本与收益
5.2 并行的力量与陷阱
权衡考量
结论
关键启示
并发与并行:现代计算中的关键概念解析
作者:zvvq博客网
免责声明:本文来源于网络,如有侵权请联系我们!