《C++ 并发编程》- 第1章 你好,C++的并发世界

简介:

本文是《C++ 并发编程》的第一章感谢人民邮电出版社授权并发编程网发表此文版权所有请勿转载。该书将于近期上市。

本章主要内容

  • 何谓并发和多线程
  •  为什么要在应用程序中使用并发和多线程
  •  C++并发支持的发展历程
  •  一个简单的C++多线程程序是什么样的

这是C++用户的振奋时刻。距1998年初始的C++标准发布13年后C++标准委员会给予程序语言和它的支持库一次重大的变革。新的C++标准也被称为C++11或C++0x于2011年发布并带来了很多的改变使得C++的应用更加容易并富有成效。

在C++11标准中一个最重要的新特性就是支持多线程程序。这是C++标准第一次在语言中承认多线程应用的存在并在库中为编写多线程应用程序提供组件。这将使得在不依赖平台相关扩展下编写多线程C++程序成为可能从而允许以有保证的行为来编写可移植的多线程代码。这也恰逢程序员更多地寻求普遍的并发特别是多线程程序来提高应用程序的性能。

这本书讲述的就是在C++编程中对多线程并发的使用以及令其成为可能的C++语言特性和库工具。我会以解释并发和多线程的含义以及为什么要在应用程序中使用并发开始。在快速绕行阐述为什么在应用程序中会不使用并发之后我会对C++中并发支持进行概述并以一个简单的C++并发实例结束这一章。具有开发多线程应用程序经验的读者可以跳过前面的小节。在随后几章会将涵盖更多广泛的例子并且更深入地了解库工具。本书最后附有多线程与并发全部的C++标准库工具的深入参考。

那么什么是并发concurrency多线程multithreading

1.1 什么是并发

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

1.1.1 计算机系统中的并发

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

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

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

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

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

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

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

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

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

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

1.1.2 并发的途径

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

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

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

多进程并发

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

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

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

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

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

多线程并发

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

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

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

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

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

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

1.2 为什么使用并发

在应用程序中使用并发的原因主要有两个关注点分离和性能。事实上我甚至可以说它们差不多是使用并发的唯一原因当你观察的足够仔细时一切其他因素都可以归结到这两者之一或者可能是二者兼有当然除了像“因为我愿意”这样的原因之外。

1.2.1 为了关注点分离而使用并发

在编写软件时关注点分离几乎总是个好主意通过将相关的代码放在一起并将无关的代码分开可以使你的程序更容易理解和测试从而减少出错的可能性。你可以使用并发来分隔不同的功能区域即使在这些不同功能区域的操作需要在同一时刻发生的穷况下若不显式地使用并发你要么被迫编写任务切换框架要么在操作中主动地调用不相关的一段代码。

考虑一类带有用户界面的密集处理型应用程序例如为台式计算机提供的DVD播放程序。这样一个应用程序基本上具备两套职能它不仅要从光盘中读取数据解码图像和声音并把它们及时输出至视频和音频硬件从而实现DVD的无错播放它还要接受来自用户的输入例如当用户单击暂停或返回菜单甚至退出按键的时候。在单个线程中应用程序须在回放期间定期检查用户的输入于是将将DVD回放代码和用户界面代码合在一起。通过使用多线程来分隔这些关注点用户界面代码和DVD回放代码不再需要如此紧密的交织在一起一个线程可以处理用户界面另一个处理DVD回放。它们之间会有交互例如用户点击暂停但现在这些交互直接与眼前的任务有关。

这会带来响应性的错觉因为用户界面线程通常可以立即响应用户的请求即使在请求被传达给干活的线程时响应为简单地显示正忙的光标或请等待的消息。类似地独立的线程常被用于运行必须在后台连续运行的任务例如在桌面搜索程序中监视文件系统的变化。以这种方式使用线程一般会使每个线程的逻辑更加简单因为它们之间的交互可以被限制为清晰可辨的点而不是到处散播不同任务的逻辑。

在这种情况下线程的数量与CPU可用内核的数量无关因为对线程的划分是基于概念上的设计而不是试图增加吞吐量。

1.2.2 为了性能而使用并发

多处理器系统已经存在了几十年但直到最近他们几乎只能在超级计算机、大型机和大型服务器系统中才能看到。然而芯片制造商越来越倾向于多核芯片的设计即在单个芯片上集成2、4、16或更多的处理器从而达到比单核心更好的性能。因此多核台式计算机甚至多核嵌入式设备现在越来越普遍。这些计算机的计算能力的提高不是源自使单一任务运行的更快而是源自并行运行多个任务。在过去程序员曾坐看他们的程序随着处理器的更新换代而变得更快无需他们这边做出任何努力。但是现在就像Herb Sutter所说的“免费的午餐结束了。”[1]如果软件想要利用日益增长的计算能力它必须设计为并发运行多个任务。程序员因此必须留意而且那些迄今都忽略并发的人们在必须注意它并将其加入他们的工具箱中。

有两种方式为了性能使用并发。首先也是最明显的是将一个单个任务分成几部分且各自并行运行从而降低总运行时间。这就是任务并行task parallelism。虽然这听起来很直观但它可以是一个相当复杂的过程因为在各个部分之间可能存在很多的依赖。区别可能是在过程方面——一个线程执行算法的一部分而另一个线程执行算法的另一个部分——或是在数据方面——每个线程在不同的数据部分上执行相同的操作。后一种方法被称为数据并行data parallelism

容易受这种并行影响的算法常被称为易并行embarrassingly parallel。抛开你可能会尴尬地面对很容易并行化的代码这一含义这是一件好事情我曾遇到过的关于此算法的别的术语是自然并行naturally parallel便利并发conveniently concurrent。易并行算法具有良好的可扩展特性——随着可用硬件线程数量的提升算法的并行性可以随之增加与之匹配。这样的一个算法是谚语“人多力量大”的完美体现。对于非易并行算法的那一部分你可以将算法划分为一个固定因而不可扩展数量的并行任务。在线程之间划分任务的技巧涵盖在第8章中。

使用并发来提升性能的第二种方法是使用可用的并行方式来解决更大的问题与其同时处理一个文件不如酌情处理2个或10个或20个。虽然这实际上只是数据并行的一种应用通过对多组数据同时执行相同的操作但还是有不同的重点。处理一个数据块仍然需要同样的时间但在相同的时间内却可以处理更多的数据。当然这种方法也存在限制且并非在所有情况下都是有益的但是这种方法所带来的吞吐量提升可以让一些新玩意变得可能例如如果图片的各部分可以并行处理就能提高视频处理的分辨率。

1.2.3 什么时候不使用并发

知道何时不使用并发与知道何时使用它一样重要。基本上不使用并发的唯一原因就是在收益比不上成本的时候。使用并发的代码在很多情况下难以理解因此编写和维护的多线程代码就有直接的脑力成本同时额外的复杂性也可能导致更多的错误。除非潜在的性能增益足够大或关注点分离地足够清晰能抵消确保其正确所需的额外的开发时间以及与维护多线程代码相关的额外成本否则不要使用并发。

同样地性能增益可能不会如预期的那么大在启动线程时存在固有的开销因为操作系统必须分配相关的内核资源和堆栈空间然后将新线程加入调度器中所有这一切都占用时间。如果在线程上运行的任务完成得很快那么任务实际上占据的时间与启动线程的开销时间相比显得微不足道可能会导致应用程序的整体性能还不如通过产生线程直接执行该任务。

此外线程是有限的资源。如果让太多的线程同时运行则会消耗操作系统资源并且使得操作系统整体上运行得更缓慢。不仅如此运行太多的线程会耗尽进程的可用内存或地址空间因为每个线程都需要一个独立的堆栈空间。对于一个可用地址空间限制为4GB的扁平架构的32位进程来说这尤其是个问题如果每个线程都有一个1MB的堆栈对于很多系统来说是典型的那么4096个线程将会用尽所有地址空间不再为代码、静态数据或者堆数据留有空间。虽然64位或者更大的系统不存在这种直接的地址空间限制它们仍然只具备有限的资源如果你运行太多的线程最终会导致问题。尽管线程池参见第9章可以用来限制线程的数量但这并不是灵丹妙药它们也有它们自己的问题。

如果客户端/服务器应用程序的服务器端为每一个链接启动一个独立的线程对于少量的链接是可以正常工作的但当同样的技术用于需要处理大量链接的高需求服务器时就会因为启动太多线程而迅速耗尽系统资源。在这种场景下谨慎地使用线程池可以提供优化的性能参见第9章。

最后运行越多的线程操作系统就需要做越多的上下文切换。每个上下文切换都需要耗费本可以花在有价值工作上的时间所以在某些时候增加一个额外的线程实际上会降低而不是提高应用程序的整体性能。为此如果你试图得到系统的最佳性能考虑可用的硬件并发或缺乏之并调整运行线程的数量是必需的。

为了性能而使用并发就像所有其他优化策略一样它拥有极大提高应用程序性能的潜力但它也可能使代码复杂化使其更难理解和更容易出错。因此只有对应用程序中的那些具有显著增益潜力的性能关键部分才值得这样做。当然如果性能收益的潜力仅次于设计清晰或关注点分离可能也值得使用多线程设计。

假设你已经决定确实要在应用程序中使用并发无论是为了性能、关注点分离或是因为“多线程星期一”对于C++程序员来说意味着什么

1.3 在C++中使用并发和多线程

通过多线程为并发提供标准化的支持对C++来说是新鲜事物。只有在即将到来的C++11标准中你才能不依赖平台相关的扩展来编写多线程代码。为了理解新版本C++线程库中众多规则背后的基本原理了解其历史是很重要的。

1.3.1 C++多线程历程

1998 C++标准版不承认线程的存在并且各种语言要素的操作效果都以顺序抽象机的形式编写。不仅如此内存模型也没有被正式定义所以对于1998 C++标准你没办法在缺少编译器相关扩展的情况下编写多线程应用程序。

当然编译器供应商可以自由地向语言添加扩展并且针对多线程的C API的流行——例如在POSIX C和Microsoft Windows API中的那些——导致很多C++编译器供应商通过各种平台相关的扩展来支持多线程。这种编译器支持普遍地受限于只允许使用该平台相应的C API以及确保该C++运行时库例如异常处理机制的代码在多线程存在的情况下运行。尽管极少有编译器供应商提供了一个正式的多线程感知内存模型但编译器和处理器的实际表现也已经足够好以至于大量的多线程的C++程序已被编写出来。

由于不满足于使用平台相关的C API来处理多线程C++程序员曾期望他们的类库提供面向对象的多线程工具。像MFC这样的应用程序框架以及像Boost和ACE这样的C++通用C++类库曾积累了多套C++类封装了下层的平台相关API并提供高级的多线程工具以简化任务。各类库的具体细节特别是在启动新线程的方面存在很大差异但是这些类的总体构造存在很多共通之处。有一个为许多C++类库共有的同时也是为程序员提供很大便利的特别重要的设计就是带锁的资源获得即初始化RAII, Resource Acquisition Is Initialization的习惯用法来确保当退出相关作用域的时候互斥元被解锁。

许多情况下现有的C++编译器所提供的多线程支持例如Boost和ACE综合了平台相关API以及平台无关类库的可用性为编写多线程C++代码提供一个坚实的基础也因此大约有数百万行C++代码作为多线程应用程序的一部分而被编写出来。但缺乏标准的支持意味着存在缺少线程感知内存模型从而导致问题的场合特别是对于那些试图通过使用处理器硬件能力来获取更高性能或是编写跨平台代码但是在不同平台之间编译器的实际表现存在差异的情况。

1.3.2 新标准中的并发支持

所有这些都随着新的C++11标准的发布而改变了。不仅有了一个全新的线程感知内存模型C++标准库也被扩展了包含了用于管理线程参见第2章、保护共享数据参见第3章、线程间同步操作参见第4章以及低级原子操作参见第5章的各个类。

新的C++线程库很大程度上基于之前通过使用上文提到的C++类库而积累的经验。特别地Boost线程库被用作新类库所基于的主要模型很多类与Boost中的对应者共享命名和结构。在新标准演进的过程中这是个双向流动Boost线程库也改变了自己以便在多个方面匹配C++标准因此从Boost迁移过来的用户将会发现自己非常习惯。

正如本章开篇提到的那样对并发的支持仅仅是新C++标准的变化之一此外还存在很多对于编程语言自身的改善可以使得程序员们的工作更便捷。这些内容虽然不在本书的论述范围之内但是其中的一些变化对于线程库本身及其使用方式已经形成了直接的冲击。附录A对这些语言特性做了简要的介绍。

C++中对原子操作的直接支持允许程序员编写具有确定语义的高效代码而无需平台相关的汇编语言。这对于那些试图编写高效的、可移植代码的程序员们来说是一个真正的福利不仅有编译器可以搞定平台的具体内容还可以编写优化器来考虑操作的语义从而让程序作为一个整体得到更好的优化。

1.3.3 C++线程库的效率

对于C++整体以及包含低级工具的C++类——特别是在新版C++线程库里的那些参与高性能计算的开发者常常关注的一点就是效率。如果你正寻求极致的性能那么理解与直接使用底层的低级工具相比使用高级工具所带来的实现成本是很重要的。这个成本就是抽象惩罚abstraction penalty

C++标准委员会在整体设计C++标准库以及专门设计标准C++线程库的时候就已经十分注重这一点了其设计的目标之一就是在提供相同的工具时通过直接使用低级API就几乎或完全得不到任何好处。因此该类库被设计为在大部分主要平台上都能高效实现带有非常低的抽象惩罚。

C++标准委员会的另一个目标是确保C++能提供足够的低级工具给那些希望与硬件工作得更紧密的程序员以获取终极性能。为了达到这个目的伴随着新的内存模型出现了一个全面的原子操作库用于直接控制单个位、字节、线程间同步以及所有变化的可见性。这些原子类型和相应的操作现在可以在很多地方加以使用而这些地方以前通常被开发者选择下放到平台相关的汇编语言中。使用了新的标准类型和操作的代码因而具有更佳的可移植性并且更易于维护。

C++标准库也提供了更高级别的抽象和工具它们使得编写多线程代码更简单和不易出错。有时候运用这些工具确实会带来性能成本因为必须执行额外的代码。但是这种性能成本并不一定意味着更高的抽象惩罚总体来看这种性能成本并不比通过手工编写等效的函数而招致的成本更高同时编译器可能会很好地内联大部分额外的代码。

在某些情况下高级工具提供超出特定使用需求的额外功能。在大部分情况下这都不是问题你没有为你不使用的那部分买单。在罕见的情况下这些未使用的功能会影响其他代码的性能。如果你更看重程序的性能且代价过高你可能最好是通过较低级别的工具来手工实现需要的功能。在绝大多数情况下额外增加的复杂性和出错的几率远大于小小的性能提升带来的潜在收益。即使有证据确实表明瓶颈出现在C++标准库的工具中这也可能归咎于低劣的应用程序设计而非低劣的类库实现。例如如果过多的线程竞争一个互斥元这将会显著影响性能。与其试图在互斥操作上刮掉一点点的时间还不如重新构造应用程序以减少互斥元上的竞争来的划算。设计应用程序以减少竞争会在第8章中加以阐述。

在非常罕见的情况下C++标准库不提供所需的性能或行为这时则有必要运用使用平台相关的工具。

1.3.4 平台相关的工具

虽然C++线程库为多线程和并发处理提供了颇为全面的工具但是在所有的平台上都会有些额外的平台相关工具。为了能方便的访问那些工具而又不用放弃使用标准C++线程库带来的好处C++线程库中的类型可以提供一个native_handle()成员函数允许通过使用平台相关API直接操作底层实现。就其本质而言任何使用native_handle()执行的操作是完全依赖于平台的这也超出了本书同时也是标准C++库本身的范围。

当然在考虑使用平台相关的工具之前明白标准库能够提供什么是很重要的那么让我们通过一个例子来开始。

1.4 开始入门

好现在你有一个很棒的与C++11兼容的编译器。接下来呢一个多线程C++程序是什么样子的它看上去和其他所有C++程序一样通常是变量、类以及函数的组合。唯一真正的区别在于某些函数可以并发运行所以你需要确保共享数据的并发访问是安全的详见第3章。当然为了并发地运行函数必须使用特定的函数以及对象来管理各个线程。

1.4.1 你好并发世界

让我们从一个经典的例子开始一个打印“Hello World.”的程序。一个非常简单的在单线程中运行的Hello, World程序如下所示当我们谈到多线程时它可以作为一个基准。


1 #include <iostream>
2 int main()
3 {
4     std::cout << "Hello World\n";
5 }


这个程序所做的一切就是将“Hello World”写进标准输出流。让我们将它与下面清单所示的简单的Hello, Concurrent World程序做个比较它启动了一个独立的线程来显示这个信息。

清单 1.1一个简单的Hello, Concurrent World程序


 1 #include <iostream>
 2 #include <thread>  //①
 3 void hello()  //②
 4 {
 5     std::cout << "Hello Concurrent World\n";
 6 }
 7 int main()
 8 {
 9     std::thread t(hello);  //③
10     t.join();  //④
11 }


第一个区别是增加了#include<thread>①。在标准C++库中对多线程支持的声明在新的头文件中用于管理线程的函数和类在<thread>中声明而那些保护共享数据的函数和类在其他头文件中声明。

其次写信息的代码被移动到了一个独立的函数中②。这是因为每个线程都必须具有一个初始函数initial function新线程的执行在这里开始。对于应用程序来说初始线程是main()但是对于所有其他线程这在std::thread对象的构造函数中指定──在本例中被命名为t③的std::thread对象拥有新函数hello()作为其初始函数。

下一个区别与直接写入标准输出或是从main()调用hello()不同该程序启动了一个全新的线程来实现将线程数量一分为二──初始线程始于main()而新线程始于hello()

在新的线程启动之后③初始线程继续执行。如果它不等待新线程结束它就将自顾自地继续运行到main()的结束从而结束程序──有可能发生在新线程有机会运行之前。这就是为什么在④这里调用join()的原因──详见第2章这会导致调用线程在main()中等待与std::thread对象相关联的线程即这个例子中的t

如果这看起来像是仅仅为了将一条信息写入标准输出而做了大量的工作那么它确实如此──正如上文1.2.3节所描述的一般来说并不值得为了如此简单的任务而使用多线程尤其是如果在这期间初始线程无所事事。在本书后面的内容中我们将通过实例来展示在哪些情景下使用多线程可以获得明确的收益。

1.5 小结

在本章中我提及了并发与多线程的含义以及在你的应用程序中为什么你会选择使用或不使用它。我还提及了多线程在C++中的发展历程从1998标准中完全缺乏支持经历了各种平台相关的扩展再到新的C++11标准中具有合适的多线程支持。该支持到来的正是时候它使得程序员们可以利用随着新的CPU而带来的更加强大的硬件并发因为芯片制造商选择了以多核心的形式使得更多任务可以同时执行的方式来增加处理能力而不是增加单个核心的执行速度。

我在1.4节中的示例来展示了C++标准库中的类和函数有多么的简单。在C++中使用多线程本身并不复杂复杂的是如何设计代码以实现其预期的行为。

在尝试了1.4节的示例之后是时候看看更多实质性的内容了。在第2章中我们将看一看用于管理线程的类和函数。


[1] “The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software,” Herb Sutter, Dr. Dobb’s

Journal, 30(3), March 2005. http://www.gotw.ca/publications/concurrency-ddj.htm.

目录
相关文章
|
28天前
|
存储 前端开发 Java
【C++ 多线程 】C++并发编程:精细控制数据打印顺序的策略
【C++ 多线程 】C++并发编程:精细控制数据打印顺序的策略
43 1
|
29天前
|
存储 前端开发 算法
C++线程 并发编程:std::thread、std::sync与std::packaged_task深度解析(一)
C++线程 并发编程:std::thread、std::sync与std::packaged_task深度解析
42 0
|
6月前
|
消息中间件 存储 前端开发
[笔记]C++并发编程实战 《四》同步并发操作(三)
[笔记]C++并发编程实战 《四》同步并发操作(三)
|
25天前
|
算法 C++ 开发者
【C++ 20 并发工具 std::barrier】掌握并发编程:深入理解C++的std::barrier
【C++ 20 并发工具 std::barrier】掌握并发编程:深入理解C++的std::barrier
40 0
|
29天前
|
存储 并行计算 Java
C++线程 并发编程:std::thread、std::sync与std::packaged_task深度解析(二)
C++线程 并发编程:std::thread、std::sync与std::packaged_task深度解析
62 0
|
30天前
|
存储 前端开发 安全
【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(三)
【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索
21 0
|
30天前
|
存储 设计模式 前端开发
【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(二)
【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索
28 0
|
30天前
|
并行计算 前端开发 安全
【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索(一)
【C++并发编程】std::future、std::async、std::packaged_task与std::promise的深度探索
59 0
|
30天前
|
并行计算 Java 调度
C/C++协程编程:解锁并发编程新纪元
C/C++协程编程:解锁并发编程新纪元
92 0
|
5月前
|
安全 前端开发 程序员
C++11 并发编程基础(一):并发、并行与C++多线程
C++11标准在标准库中为多线程提供了组件,这意味着使用C++编写与平台无关的多线程程序成为可能,而C++程序的可移植性也得到了有力的保证。另外,并发编程可提高应用的性能,这对对性能锱铢必较的C++程序员来说是值得关注的。
110 0
C++11 并发编程基础(一):并发、并行与C++多线程