《C++并发编程实战》——1.1 什么是并发

1.1 什么是并发

在最简单和最基本的层面,并发是指两个或更多独立的活动同时发生。并发在生活中随处可见。我们可以一边走路一边说话,也可以两只手同时做不同的动作,还有我们每个人都相互独立地过我们的生活——我在游泳的时候你可以看球赛,等等。

1.1.1 计算机系统中的并发

当我们提到计算机术语的“并发”,指的是在单个系统里同时执行多个独立的活动,而不是顺序地或是一个接一个地。这并不是一种新的现象,多任务操作系统通过任务切换允许一台计算机在同一时间运行多个应用程序已司空见惯多年,一些高端的多任务处理服务器实现并发控制的历史更久远。真正有新意的是增加计算机真正并行运行多任务的普遍性,而不只是给人这种错觉。

以前,大多数计算机都有一个处理器,具有单个处理单元或核心,至今许多台式机器仍是这样。这种计算机在某一时刻只可以真正执行一个任务,但它可以每秒切换任务许多次。通过做一点这个任务然后再做一点别的任务,看起来像是任务在并行发生。这就是任务切换(taskswitching)。我们仍然将这样的系统称为并发(concurrency),因为任务切换得太快,以至于无法分辨任务在何时会被暂挂而切换到另一个任务。任务切换给用户和应用程序本身造成了一种并发的假象。由于这只是并发的假象,当应用程序执行在单处理器任务切换环境下,与在真正的并发环境下执行相比,其行为还是有着微妙的不同。特别地,对内存模型不正确的假设(参见第5章)在这样的环境中可能不会出现。这将在第10章中作深入讨论。

包含多个处理器的计算机用于服务器和高性能计算任务已有多年,现在基于单个芯片上具有多于一个核心的处理器(多核心处理器)的计算机也成为越来越常见的台式机器。无论它们拥有多个处理器或一个多核处理器(或两者兼具),这些计算机能够真正的并行运行超过一个任务。我们才称之为硬件并发(hardwareconcurrency)。

图1.1显示了一个计算机处理恰好两个任务时的理想情景,每个任务被分为10个相等大小的块。在一个双核机器(具有两个处理核心)中,每个任务可以在各自的核心执行。在单核机器上做任务切换时,每个任务的块交织进行。但它们也隔开了一位(图中所示灰色分隔条的厚度大于双核机器的分隔条)。为了实现交替进行,该系统每次从一个任务切换到另一个时都得执行一次上下文切换(contextswitch),而这是需要时间的。为了执行上下文切换,操作系统必须为当前运行的任务保存CPU的状态和指令指针,算出要切换到哪个任务,并为要切换到的任务重新加载处理器状态。然后CPU可能要将新任务的指令和数据的内存载入到缓存中,这可能会阻止CPU执行任何指令,造成进一步的延迟。

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

尽管硬件并发的可用性在多处理器或多核系统上更显著,有些处理器却可以在一个核心上执行多个线程。要考虑的最重要的因素是硬件线程(hardwarethreads)的数量:即硬件可以真正并发运行多少独立的任务。即便是具有真正硬件并发的系统,也很容易有超过硬件可并行运行的任务要执行,所以在这些情况下任务切换仍将被使用。例如,在一个典型的台式计算机上可能会有几百个的任务在运行,执行后台操作,即使计算机在名义上是空闲的。正是任务切换使得这些后台任务可以运行,并使得你可以同时运行文字处理器、编译器、编辑器和web浏览器(或任何应用的组合)。图1.2显示了四个任务在一台双核机器上的任务切换,仍然是将任务整齐地划分为同等大小块的理想情况。实际上,许多因素造成了分割不均和调度不规则。这些因素中的一部分将涵盖在第8章中,那时我们再来看一看影响并行代码性能的因素。

图1.2 四个任务在两个核心之间的切换

所有的技术、功能和本书所涉及的类都可以使用,无论你的应用程序是在单核处理器还是多核处理器上运行,也不管是任务切换或是真正的硬件并发。但你可以想象,如何在你的应用程序中使用并发很大程度上取决于可用的硬件并发。这将在第8章中涵盖,在第8章我们具体研究C++代码并行设计问题。

1.1.2 并发的途径

想象一下两个程序员一起做一个软件项目。如果你的开发人员在独立的办公室,它们可以各自平静地工作,而不会互相干扰,并且他们各有自己的一套参考手册。然而,沟通起来就不那么直接了;不能转身然后互相交谈,他们必须用电话、电子邮件或走到对方的办公室。同时,你需要掌控两个办公室的开销,还要购买多份参考手册。

现在想象一下把开发人员移到同一间办公室。他们现在可以地相互交谈来讨论应用程序的设计,他们也可以很容易地用纸或白板来绘制图表,辅助阐释设计思路。你现在只有一个办公室要管理,只要一组资源就可以满足。消极的一面是,他们可能会发现难以集中注意力,并且还可能存在资源共享的问题(“参考手册跑哪去了?”)。

组织开发人员的这两种方法代表着并发的两种基本途径。每个开发人员代表一个线程,每个办公室代表一个处理器。第一种途径是有多个单线程的进程,这就类似让每个开发人员在他们自己的办公室,而第二种途径是在单一进程里有多个线程,这就类似在同一个办公室里有两个开发人员。你可以随意进行组合,并且拥有多个进程,其中一些是多线程的,一些是单线程的,但原理是一样的。让我们在一个应用程序中简要地看一看这两种途径。

1.多进程并发
在一个应用程序中使用并发的第一种方法,是将应用程序分为多个、独立的、单线程的进程,它们运行在同一时刻,就像你可以同时进行网页浏览和文字处理。这些独立的进程可以通过所有常规的进程间通信渠道互相传递信息(信号、套接字、文件、管道等),如图1.3所示。有一个缺点是这种进程之间的通信通常设置复杂,或是速度较慢,或两者兼备,因为操作系统通常在进程间提供了大量的保护,以避免一个进程不小心修改了属于另一个进程的数据。另一个缺点是运行多个进程所需的固有的开销:启动进程需要时间,操作系统必须投入内部资源来管理进程,等等。

图1.3 一对并发运行的进程之间的通信

当然,也并不全是缺点:操作系统在线程间提供的附加保护操作和更高级别的通信机制,意味着可以比线程更容易地编写安全的并发代码。事实上,类似于为Erlang编程语言提供的环境,可使用进程作为重大作用并发的基本构造块。

使用独立的进程实现并发还有一个额外的优势——你可以通过网络连接的不同的机器上运行独立的进程。虽然这增加了通信成本,但在一个精心设计的系统上,它可能是一个提高并行可用行和提高性能的低成本方法。

2.多线程并发
并发的另一个途径是在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独立运行,且每个线程可以运行不同的指令序列。但进程中的所有线程都共享相同的地址空间,并且从所有线程中访问到大部分数据——全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。虽然通常可以在进程之间共享内存,但这难以建立并且通常难以管理,因为同一数据的内存地址在不同的进程中也不尽相同。图1.4显示了一个进程中的两个线程通过共享内存进行通信。

图1.4 同一进程中的一对并发运行的线程之间的通信

共享的地址空间,以及缺少线程间的数据保护,使得使用多线程相关的开销远小于使用多进程,因为操作系统有更少的簿记要做。但是,共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么程序员必须确保当每个线程访问时所看到的数据是一致的。线程间数据共享可能会遇到的问题、所使用的工具以及为了避免问题而要遵循的准则在本书中都有涉及,特别是在第3、4、5和8章中。这些问题并非不能克服,只要在编写代码时适当地注意即可,但这却意味着必须对线程之间的通信作大量的思考。

相比于启动多个单线程进程并在其间进行通信,启动单一进程中的多线程并在其间进行通信的开销更低,这意味着若不考虑共享内存可能会带来的潜在问题,它是包括C++在内的主流语言更青睐的并发途径。此外,C++标准没有为进程间通信提供任何原生支持,所以使用多进程的应用程序将不得不依赖平台相关的API来实现。因此,本书专门关注使用多线程的并发,并且之后提到并发均是假定通过使用多线程来实现的。

明确了什么是并发后,现在让我们来看看为什么要在应用程序中使用并发。

时间: 2024-08-03 04:17:21

《C++并发编程实战》——1.1 什么是并发的相关文章

《GO并发编程实战》—— 原子操作

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们已经知道,原子操作即是进行过程中不能被中断的操作.也就是说,针对某个值的原子操作在被进行的过程当中,CPU绝不会再去进行其它的针对该值的操作.无论这些其它的操作是否为原子操作都会是这样.为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成.只有这样才能够在并发环境下保证原子操作的绝对安全. Go语言提供的原子操作都是非侵入式的.它们由标准库代码包sync/atomic中的众多函

《Java 7并发编程实战手册》第六章并发集合

由人民邮电出版社出版的<Java 7并发编程实战手册>终于出版了,译者是俞黎敏和申绍勇,该书将于近期上架.之前并发编程网组织翻译过此书,由于邮电出版社在并发网联系他们之前就找到了译者,所以没有采用并发网的译稿,但邮电出版社将于并发网展开合作,发布该书的样章(样章由并发网挑选,你也可以回帖告诉我们你想看哪一章的样章),并组织赠书活动回馈给活跃读者.活动详情请时刻关注并发网的微博和微信(微信号:ifeves),最后祝各位用餐愉快!:) 本章将介绍下列内容: 使用非阻塞式线程安全列表 使用阻塞式线程

《GO并发编程实战》—— 条件变量

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们在第6章讲多线程编程的时候详细说明过条件变量的概念.原理和适用场景.因此,我们在本小节仅对sync代码包中与条件变量相关的API进行简单的介绍,并使用它们来改造我们之前实现的*myDataFile类型的相关方法. 在Go语言中,sync.Cond类型代表了条件变量.与互斥锁和读写锁不同,简单的声明无法创建出一个可用的条件变量.为了得到这样一个条件变量,我们需要用到sync.NewCond函数

《GO并发编程实战》—— 锁的使用

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 在本节,我们对Go语言所提供的与锁有关的API进行说明.这包括了互斥锁和读写锁.我们在第6章描述过互斥锁,但却没有提到过读写锁.这两种锁对于传统的并发程序来说都是非常常用和重要的. 互斥锁 互斥锁是传统的并发程序对共享资源进行访问控制的主要手段.它由标准库代码包sync中的Mutex结构体类型代表.sync.Mutex类型(确切地说,是*sync.Mutex类型)只有两个公开方法--Lock和U

《GO并发编程实战》—— Concurrent Map

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们在本章前面的部分中对Go语言提供的各种传统同步工具和方法进行了逐一的介绍.在本节,我们将运用它们来构造一个并发安全的字典(Map)类型. 我们已经知道,Go语言提供的字典类型并不是并发安全的.因此,我们需要使用一些同步方法对它进行扩展.这看起来并不困难.我们只要使用读写锁将针对一个字典类型值的读操作和写操作保护起来就可以了.确实,读写锁应该是我们首先想到的同步工具.不过,我们还不能确定只使用

《GO并发编程实战》—— 只会执行一次

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 现在,让我们再次聚焦到sync代码包.除了我们介绍过的互斥锁.读写锁和条件变量,该代码包还为我们提供了几个非常有用的API.其中一个比较有特色的就是结构体类型sync.Once和它的Do方法. 与代表锁的结构体类型sync.Mutex和sync.RWMutex一样,sync.Once也是开箱即用的.换句话说,我们仅需对它进行简单的声明即可使用,就像这样: var once sync.Once o

《GO并发编程实战》—— WaitGroup

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 我们在第6章多次提到过sync.WaitGroup类型和它的方法.sync.WaitGroup类型的值也是开箱即用的.例如,在声明 var wg sync.WaitGroup 之后,我们就可以直接正常使用wg变量了.该类型有三个指针方法,即Add.Done和Wait. 类型sync.WaitGroup是一个结构体类型.在它之中有一个代表计数的字段.当一个sync.WaitGroup类型的变量被声

《GO并发编程实战》—— 临时对象池

声明:本文是<Go并发编程实战>的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文. 本章要讲解的是sync.Pool类型.我们可以把sync.Pool类型值看作是存放可被重复使用的值的容器.此类容器是自动伸缩的.高效的,同时也是并发安全的.为了描述方便,我们也会把sync.Pool类型的值称为临时对象池,而把存于其中的值称为对象值.至于为什么要加"临时"这两个字,我们稍后再解释. 我们在用复合字面量初始化一个临时对象池的时候可以为它唯一的公开字段New赋值.

《C++并发编程实战》——第1章 你好,C++并发世界

第1章 你好,C++并发世界 C++并发编程实战本章主要内容 何谓并发和多线程为什么要在应用程序中使用并发和多线程C++并发支持的发展历程一个简单的C++多线程程序是什么样的这是令C++用户振奋的时刻.距1998年初始的C++标准发布13年后,C++标准委员会给予程序语言和它的支持库一次重大的变革.新的C++标准(也被称为C++11或C++0x)于2011年发布并带来了很多的改变,使得C++的应用更加容易并富有成效. 在C++11标准中一个最重要的新特性就是支持多线程程序.这是C++标准第一次在

《Java 7并发编程实战手册》第四章线程执行器

感谢人民邮电大学授权并发网发布此书样章,新书购买传送门=>当当网 本章将介绍下列内容: 创建线程执行器 创建固定大小的线程执行器 在执行器中执行任务并返回结果 运行多个任务并处理第一个结果 运行多个任务并处理所有结果 在执行器中延时执行任务 在执行器中周期性执行任务 在执行器中取消任务 在执行器中控制任务的完成 在执行器中分离任务的启动与结果的处理 处理在执行器中被拒绝的任务 4.1 简介 通常,使用Java来开发一个简单的并发应用程序时,会创建一些 Runnable 对象,然后创建对应的 Th