什么是并发?为什么使用并发技术?

人邮异步社区 2021-12-02 19:37:33

1 什么是并发

按最简单、最基本的程度理解,并发(concurrency)是两个或多个同时独立进行的活动。并发现象遍布日常生活,我们时常接触:我们可以边走路边说话;或者,左右手同时做出不一样的动作;我们每个人也都可以独立行事——当我游泳时,你可以观看足球比赛;诸如此类。

1.1 计算机系统中的并发

若我们谈及计算机系统中的并发,则是指同一个系统中,多个独立活动同时进行,而非依次进行。这不足为奇。多年来,多任务操作系统可以凭借任务切换,让同一台计算机同时运行多个应用软件,这早已稀松平常,而高端服务器配备了多处理器,实现了“真并发”(genuine concurrency)。大势所趋,主流计算机现已能够真真正正地并行处理多任务,而不再只是制造并发的表象。

很久之前,大多计算机都仅有一个处理器,处理器内只有单一处理单元或单个内核,许多台式计算机至今依旧如此。这种计算机在同一时刻实质上只能处理一个任务,不过,每秒内,它可以在各个任务之间多次切换,先处理某任务的一小部分,接着切换任务,同样只处理一小部分,然后对其他任务如法炮制。于是,看起来所有任务都正在同时执行。因此其被称为任务切换。至此,我们谈及的并发都基于这种模式。由于任务飞速切换,我们难以分辨处理器到底在哪一刻暂停某个任务而切换到另一个。任务切换对使用者和应用软件自身都制造出并发的表象。由于是表象,因此对比真正的并发环境,当应用程序在进行任务切换的单一处理器环境下运行时,其行为可能稍微不同。具体而言,如果就内存模型(见第5章)做出不当假设,本来会导致某些问题,但这些问题在上述环境中却有可能不会出现。第10章将对此深入讨论。

多年来,配备了多处理器的计算机一直被用作服务器,它要承担高性能的计算任务;现今,基于一芯多核处理器(简称多核处理器)的计算机日渐普及,多核处理器也用在台式计算机上。无论是装配多个处理器,还是单个多核处理器,或是多个多核处理器,这些计算机都能真正并行运作多个任务,我们称之为硬件并发(hardware concurrency)。

图1.1所示为理想化的情景。计算机有两个任务要处理,将它们进行十等分。在双核机(具有两个处理核)上,两个任务在各自的核上分别执行。另一台单核机则切换任务,交替执行任务小段,但任务小段之间略有间隔。在图1.1中,单核机的任务小段被灰色小条隔开,它们比双核机的分隔条粗大。为了交替执行,每当系统从某一个任务切换到另一个时,就必须完成一次上下文切换(context switch),于是耗费了时间。若要完成一次上下文切换,则操作系统需保存当前任务的CPU状态和指令指针[2],判定需要切换到哪个任务,并为之重新加载CPU状态。接着,CPU有可能需要将新任务的指令和数据从内存加载到缓存,这或许会妨碍CPU,令其无法执行任何指令,加剧延迟。

图1.1 两种并发方式:双核机上的并发执行与单核机上的任务切换

尽管多处理器或多核系统明显更适合硬件并发,不过有些处理器也能在单核上执行多线程。真正需要注意的关键因素是硬件支持的线程数(hardware threads),也就是硬件自身真正支持同时运行的独立任务的数量。即便是真正支持硬件并发的系统,任务的数量往往容易超过硬件本身可以并行处理的数量,因而在这种情形下任务切换依然有用。譬如,常见的台式计算机能够同时运行数百个任务,在后台进行各种操作,表面上却处于空闲状态。正是由于任务切换,后台任务才得以运作,才容许我们运行许多应用软件,如文字处理软件、编译器、编辑软件,以及浏览器等。图1.2展示了双核机上4个任务的相互切换,这同样是理想化的情形,各个任务都被均匀切分。实践中,许多问题会导致任务切分不均匀或调度不规则。我们将在第8章探究影响并发代码性能的因素,将解决上述某些问题。

图1.2 4个任务在双核机上切换

本书涉及的技术、函数和类适用于各种环境:无论负责运行的计算机是配备了单核单处理器,还是多核多处理器;无论其并发功能如何实现,是凭借任务切换,还是真正的硬件并发,一概不影响使用。然而,也许读者会想到,应用软件如何充分利用并发功能,很大程度上取决于硬件所支持的并发任务数量。我们将在第8章讲述设计并发的C++代码的相关议题,也会涉及这点。

1.2 并发的方式

设想两位开发者要共同开发一个软件项目。假设他们处于两间独立的办公室,而且各有一份参考手册,则他们可以静心工作,不会彼此干扰。但这令交流颇费周章:他们无法一转身就与对方交谈,遂不得不借助电话或邮件,或是需起身离座走到对方办公室。另外,使用两间办公室有额外开支,还需购买多份参考手册。

现在,如果安排两位开发者共处一室,他们就能畅谈软件项目的设计,也便于在纸上或壁板上作图,从而有助于交流设计的创意和理念。这样,仅有一间办公室要管理,并且各种资源通常只需一份就足够了。但缺点是,他们恐怕难以集中精神,共享资源也可能出现问题。

这两种安排开发者的办法示意了并发的两种基本方式。一位开发者代表一个线程,一间办公室代表一个进程。第一种方式采用多个进程,各进程都只含单一线程,情况类似于每位开发者都有自己的办公室;第二种方式只运行单一进程,内含多个线程,正如两位开发者同处一间办公室。我们可以随意组合这两种方式,掌控多个进程,其中有些进程包含多线程,有些进程只包含单一线程,但基本原理相同。接着,我们来简略看看应用软件中的这两种并发方式。

1.多进程并发

在应用软件内部,一种并发方式是,将一个应用软件拆分成多个独立进程同时运行,它们都只含单一线程,非常类似于同时运行浏览器和文字处理软件。这些独立进程可以通过所有常规的进程间通信途径相互传递信息(信号、套接字、文件、管道等),如图1.3所示。这种进程间通信普遍存在短处:或设置复杂,或速度慢,甚至二者兼有,因为操作系统往往要在进程之间提供大量防护措施,以免某进程意外改动另一个进程的数据;还有一个短处是运行多个进程的固定开销大,进程的启动花费时间,操作系统必须调配内部资源来管控进程,等等。

图1.3 两个进程并发运行并相互通信

进程间通信并非一无是处:通常,操作系统在进程间提供额外保护和高级通信机制。这就意味着,比起线程,采用进程更容易编写出安全的并发代码。某些编程环境以进程作为基本构建单元,其并发效果确实一流,譬如为Erlang编程语言准备的环境。

运用独立的进程实现并发,还有一个额外优势——通过网络连接,独立的进程能够在不同的计算机上运行。这样做虽然增加了通信开销,可是只要系统设计精良,此法足以低廉而有效地增强并发力度,改进性能。

2.多线程并发

另一种并发方式是在单一进程内运行多线程。线程非常像轻量级进程:每个线程都独立运行,并能各自执行不同的指令序列。不过,同一进程内的所有线程都共用相同的地址空间,且所有线程都能直接访问大部分数据。全局变量依然全局可见,指向对象或数据的指针和引用能在线程间传递。尽管进程间共享内存通常可行,但这种做法设置复杂,往往难以驾驭,原因是同一数据的地址在不同进程中不一定相同。图1.4展示了单一进程内的两个线程借共享内存通信。

图1.4 单一进程内的两个线程借共享内存通信

我们可以启用多个单线程的进程并在进程间通信,也可以在单一进程内发动多个线程而在线程间通信,后者的额外开销更低。因此,即使共享内存带来隐患,主流语言大都青睐以多线程的方式实现并发功能,当中也包括C++。再加上C++本身尚不直接支持进程间通信,所以采用多进程的应用软件将不得不依赖于平台专属的应用程序接口(Application Program Interface,API)。鉴于此,本书专攻多线程并发,后文再提及并发,便假定采用多线程实现。

提到多线程代码,还常常用到一个词——并行。接下来,我们来厘清并发与并行的区别。

1.3 并发与并行

就多线程代码而言,并发与并行(parallel)的含义很大程度上相互重叠。确实,在多数人看来,它们就是相同的。二者差别甚小,主要是着眼点和使用意图不同。两个术语都是指使用可调配的硬件资源同时运行多个任务,但并行更强调性能。当人们谈及并行时,主要关心的是利用可调配的硬件资源提升大规模数据处理的性能;当谈及并发时,主要关心的是分离关注点或响应能力。这两个术语之间并非泾渭分明,它们之间仍有很大程度的重叠,知晓这点会对后文的讨论有所帮助,两者的范例将穿插本书。

至此,我们已明晰并发的含义,现在来看看应用软件为什么要使用并发技术。

2 为什么使用并发技术

应用软件使用并发技术的主要原因有两个:分离关注点与性能提升。据我所知,实际上这几乎是仅有的用到并发技术的原因。如果寻根究底,任何其他原因都能归结为二者之一,也可能兼有(除非硬要说“因为我就是想并发”)。

2.1 为分离关注点而并发

一直以来,编写软件时,分离关注点(separation of concerns)几乎总是不错的构思:归类相关代码,隔离无关代码,使程序更易于理解和测试,因此所含缺陷很可能更少。并发技术可以用于隔离不同领域的操作,即便这些不同领域的操作需同时进行;若不直接使用并发技术,我们将不得不编写框架做任务切换,或者不得不在某个操作步骤中,频繁调用无关领域的代码。

考虑一个带有用户界面的应用软件,需要由CPU密集处理,如台式计算机上的DVD播放软件。本质上,这个应用软件肩负两大职责:既要从碟片读取数据,解码声音影像,并将其及时传送给图形硬件和音效硬件,让DVD顺畅放映,又要接收用户的操作输入,譬如用户按“暂停”“返回选项单”“退出”等键。假若采取单一线程,则该应用软件在播放过程中,不得不定时检查用户输入,结果会混杂播放DVD的代码与用户界面的代码。改用多线程就可以分离上述两个关注点,一个线程只负责用户界面管理,另一个线程只负责播放DVD,用户界面的代码和播放DVD的代码遂可避免紧密纠缠。两个线程之间还会保留必要的交互,例如按“暂停”键,不过这些交互仅仅与需要立即处理的事件直接关联。

如果用户发送了操作请求,而播放DVD线程正忙,无法马上处理,那么在请求被传送到该线程的同时,代码通常能令用户界面线程立刻做出响应,即便只是显示光标或提示“请稍候”。这种方法使得应用软件看起来响应及时。类似地,某些必须在后台持续工作的任务,则常常交由独立线程负责运行,例如,让桌面搜索应用软件监控文件系统变动。此法基本能大幅简化各线程的内部逻辑,原因是线程间交互得以限定于代码中可明确辨识的切入点,而无须将不同任务的逻辑交错散置。

这样,线程的实际数量便与CPU既有的内核数量无关,因为用线程分离关注点的依据是设计理念,不以增加运算吞吐量为目的。

2.2 为性能而并发:任务并行和数据并行

多处理器系统已存在数十年,不过一直以来它们大都只见于巨型计算机、大型计算机和大型服务器系统。但是,芯片厂家日益倾向设计多核芯片,在单一芯片上集成2个、4个、16个或更多处理器,从而使其性能优于单核芯片。于是,多核台式计算机日渐流行,甚至多核嵌入式设备亦然。不断增强的算力并非得益于单个任务的加速运行,而是来自多任务并行运作。从前,处理器更新换代,程序自然而然随之加速,程序员可以“坐享其成,不劳而获”。但现在,正如Herb Sutter指出的“免费午餐没有了![3]”,软件若要利用增强的这部分算力,就必须设计成并发运行任务。所以程序员必须警觉,特别是那些踌躇不前、忽视并发技术的同业,有必要注意熟练掌握并发技术,储备技能。

增强性能的并发方式有两种。第一种,最直观地,将单一任务分解成多个部分,各自并行运作,从而节省总运行耗时。此方式即为任务并行。尽管听起来浅白、直接,但这却有可能涉及相当复杂的处理过程,因为任务各部分之间也许存在纷繁的依赖。任务分解可以针对处理过程,调度某线程运行同一算法的某部分,另一线程则运行其他部分;也可以针对数据,线程分别对数据的不同部分执行同样的操作,这被称为数据并行。

易于采用上述并行方式的算法常常被称为尴尬并行[4]算法。其含义是,将算法的代码并行化实在简单,甚至简单得会让我们尴尬,实际上这是好事。我还遇见过用其他术语描述这类算法,叫“天然并行”(naturally parallel)与“方便并发”(conveniently concurrent)。尴尬并行算法具备的优良特性是可按规模伸缩——只要硬件支持的线程数目增加,算法的并行程度就能相应提升。这种算法是成语“众擎易举”的完美体现。算法中除尴尬并行以外的部分,可以另外划分成一类,其并行任务的数目固定(所以不可按规模伸缩)。第8章和第10章将涵盖按线程分解任务的方法。

第二种增强性能的并发方式是利用并行资源解决规模更大的问题。例如,只要条件适合,便同时处理2个文件,或者10个,甚至20个,而不是每次1个。同时对多组数据执行一样的操作,实际上是采用了数据并行,其着眼点有别于任务并行。采用这种方式处理单一数据所需的时间依旧不变,而同等时间内能处理的数据相对更多。这种方式明显存在局限,虽然并非任何情形都会因此受益,但数据吞吐量却有所增加,进而带来突破。例如,若能并行处理视频影像中不同的区域,就会提升视频处理的解析度。

2.3 什么时候避免并发

知道何时避免并发,与知道何时采用并发同等重要。归根结底,不用并发技术的唯一原因是收益不及代价。多数情况下,采用了并发技术的代码更难理解,编写和维护多线程代码会更劳心费神,并且复杂度增加可能导致更多错误。编写正确运行的多线程代码需要额外的开发时间和相关维护成本,除非潜在的性能提升或分离关注点而提高的清晰度值得这些开销,否则别使用并发技术。

此外,性能增幅可能不如预期。线程的启动存在固有开销,因为系统须妥善分配相关的内核资源和栈空间,然后才可以往调度器添加新线程,这些都会耗费时间。假如子线程上运行的任务太快完成,处理任务本身的时间就会远短于线程启动的时间,结果,应用程序的整体性能很可能还不如完全由主线程直接执行任务的性能。

再者,线程是一种有限的资源。若一次运行太多线程,便会消耗操作系统资源,可能令系统整体变慢。而且,由于每个线程都需要独立的栈空间[5],如果线程太多,就可能耗尽所属进程的可用内存或地址空间。在采用扁平模式内存架构的32位进程中,可用的地址空间是4GB,这很成问题:假定每个线程栈的大小都是1MB(这个大小常见于许多系统),那么4096个线程即会把全部地址空间耗尽,使得代码、静态数据和堆数据无地立足。尽管64位系统(或指令集宽度更大的系统)对地址空间的直接限制相对宽松,但其资源依旧有限,运行太多线程仍将带来问题。虽然线程池可用于控制线程数量(见第9章),但也非万能妙法,它自身也有局限。

假设,在服务器端,客户端/服务器(Client/Server,C/S)模式的应用程序为每个连接发起一个独立的线程。如果只有少量连接,这尚能良好工作。不过,请求量巨大的服务器需要处理的连接数目庞大,若采用同样的方法,就会发起过多线程而很快耗尽系统资源。针对这一情形,如果要达到最优性能,便须谨慎使用线程池(见第9章)。

最后,运行的线程越多,操作系统所做的上下文切换就越频繁,每一次切换都会减少本该用于实质工作的时间。结果,当线程数目达到一定程度时,再增加新线程只会降低应用软件的整体性能,而不会提升性能。正因如此,若读者意在追求最优系统性能,则须以可用的硬件并发资源为依据(或反之考虑其匮乏程度),调整运行线程的数目。

为了提升性能而使用并发技术,与其他优化策略相仿:它极具提升应用程序性能的潜力,却也可能令代码复杂化,使之更难理解、更容易出错。所以,对于应用程序中涉及性能的关键部分,若其具备提升性能的潜力,收效可观,才值得为之实现并发功能。当然,如果首要目标是设计得清楚明晰或分离关注点,而提升性能居次,也值得采用多线程设计。

本文摘自《C++并发编程实战》(第2版)

C++并发编程实战(第2版)

这是一本介绍C++并发和多线程编程的深度指南。本书从C++标准程序库的各种工具讲起,介绍线程管控、在线程间共享数据、并发操作的同步、C++内存模型和原子操作等内容。同时,本书还介绍基于锁的并发数据结构、无锁数据结构、并发代码,以及高级线程管理、并行算法函数、多线程应用的测试和除错。本书还通过附录及线上资源提供丰富的补充资料,以帮助读者更完整、细致地掌握C++并发编程的知识脉络。

本书适合需要深入了解C++多线程开发的读者,以及使用C++进行各类软件开发的开发人员、测试人员,还可以作为C++线程库的参考工具书。 

 

...全文
2684 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

273

社区成员

发帖
与我相关
我的任务
社区描述
异步社区是人民邮电出版社创办的IT专业图书社区。于2015年8月上线运营,依托于人民邮电出版社30余年的IT专业优质出版资源和编辑策划团队,致力于优质学习内容的出版和分享,为作者、读者搭建起沟通桥梁。
学习 企业社区
社区管理员
  • 人邮异步社区
  • 东方不败耶耶耶
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

hi,欢迎加入人民邮电出版社异步社区读书会!

在这里你可以:

  • 获取最新图书资讯
  • 参与活动免费赠书
  • 与我们的作者、译者互动!

 

加入我们成为人民邮电出版社异步图书的作译者

【欢迎联系】

微信公众号:异步图书/异步社区

 

试试用AI创作助手写篇文章吧