4.1-chinese
4.1 等待一个事件或其他条件假设你在旅游,而且正在一辆在夜间运行的火车上。在夜间,如何在正确的站点下车呢?一种方法是整晚都要醒着,然后注意到了哪一站。这样,你就不会错过你要到达的站点,但是这样会让你感到很疲倦。另外,你可以看一下时间表,估计一下火车到达目的地的时间,然后在一个稍早的时间点上设置闹铃,然后你就可以安心的睡会了。这个方法听起来也很不错,也没有错过你要下车的站点,但是当火车晚点的时候,你就要被过早的叫醒了。当然,闹钟的电池也可能会没电了,并导致你睡过站。理想的方式是,无论是早或晚,只要当火车到站的时候,有人或其他东西能把你唤醒,就好了。 这和线程有什么关系呢?好吧,让我们来联系一下。当一个线程等待另一个线程完成任务时,它会有很多选择。第一,它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一线程完成工作时对这个标志进行重设。不过,就是一种浪费:线程消耗宝贵的执行时间持续的检查对应标志,并且当互斥量被等待线程上锁后,其他线程就没有办法获取锁,这样线程就会持续等待。因为以上方式对等待线程限制资源,并且在完成时阻碍对标识的设置。这种情况类似与,保持清醒状态和列车...
4.2-chinese
4.2 使用期望等待一次性事件假设你乘飞机去国外度假。当你到达机场,并且办理完各种登机手续后,你还需要等待机场广播通知你登机,可能要等很多个小时。你可能会在候机室里面找一些事情来打发时间,比如:读书,上网,或者来一杯价格不菲的机场咖啡,不过从根本上来说你就在等待一件事情:机场广播能够登机的时间。给定的飞机班次再之后没有可参考性;当你在再次度假的时候,你可能会等待另一班飞机。 C++标准库模型将这种一次性事件称为期望(future)。当一个线程需要等待一个特定的一次性事件时,在某种程度上来说它就需要知道这个事件在未来的表现形式。之后,这个线程会周期性(较短的周期)的等待或检查,事件是否触发(检查信息板);在检查期间也会执行其他任务(品尝昂贵的咖啡)。另外,在等待任务期间它可以先执行另外一些任务,直到对应的任务触发,而后等待期望的状态会变为就绪(ready)。一个“期望”可能是数据相关的(比如,你的登机口编号),也可能不是。当事件发生时(并且期望状态为就绪),这个“期望”就不能被重置。 在C++标准库中,有两种“期望”,使用两种类型模板实现,声明在头文件中: 唯一期望(unique ...
4.3-chinese
4.3 限定等待时间之前介绍过的所有阻塞调用,将会阻塞一段不确定的时间,将线程挂起直到等待的事件发生。在很多情况下,这样的方式很不错,但是在其他一些情况下,你就需要限制一下线程等待的时间了。这允许你发送一些类似“我还存活”的信息,无论是对交互式用户,或是其他进程,亦或当用户放弃等待,你可以按下“取消”键直接终止等待。 介绍两种可能是你希望指定的超时方式:一种是“时延”的超时方式,另一种是“绝对”超时方式。第一种方式,需要指定一段时间(例如,30毫秒);第二种方式,就是指定一个时间点(例如,协调世界时[UTC]17:30:15.045987023,2011年11月30日)。多数等待函数提供变量,对两种超时方式进行处理。处理持续时间的变量以“_for”作为后缀,处理绝对时间的变量以”_until”作为后缀。 所以,当std::condition_variable的两个成员函数wait_for()和wait_until()成员函数分别有两个负载,这两个负载都与wait()成员函数的负载相关——其中一个负载只是等待信号触发,或时间超期,亦或是一个虚假的唤醒,并且醒来时,会检查锁提供的谓词...
4.4-chinese
4.4 使用同步操作简化代码同步工具的使用在本章称为构建块,你可以之关注那些需要同步的操作,而非具体使用的机制。当需要为程序的并发时,这是一种可以帮助你简化你的代码的方式,提供更多的函数化的方法。比起在多个线程间直接共享数据,每个任务拥有自己的数据会应该会更好,并且结果可以对其他线程进行广播,这就需要使用“期望”来完成了。 4.4.1 使用“期望”的函数化编程术语函数化编程(functional programming)引用于一种编程方式,这种方式中的函数结果只依赖于传入函数的参数,并不依赖外部状态。当一个函数与数学概念相关时,当你使用相同的函数调用这个函数两次,这两次的结果会完全相同。C++标准库中很多与数学相关的函数都有这个特性,例如,sin(正弦),cos(余弦)和sqrt(平方根);当然,还有基本类型间的简单运算,例如,3+3,6*9,或1.3/4.7。一个纯粹的函数不会改变任何外部状态,并且这种特性完全限制了函数的返回值。 很容易想象这是一种什么样的情况,特别是当并行发生时,因为在第三章时我们讨论过,很多问题发生在共享数据上。当共享数据没有被修改,那么就不存在...
6.0-chinese
第6章 基于锁的并发数据结构设计本章主要内容 并发数据结构设计的意义 指导如何设计 实现为并发设计的数据结构 在上一章中,我们对底层原子操作和内存模型有了详尽的了解。在本章中,我们将先将底层的东西放在一边(将会在第7章再次提及),来对数据结构做一些讨论。 数据结构的选择,对于程序来说,是其解决方案的重要组成部分,当然,并行程序也不例外。如果一种数据结构可以被多个线程所访问,其要不就是绝对不变的(其值不会发生变化,并且不需同步),要不程序就要对数据结构进行正确的设计,以确保其能在多线程环境下能够(正确的)同步。一种选择是使用独立的互斥量,其可以锁住需要保护的数据(这种方法已经在第3和第4章中提到),另一种选择是设计一种能够并发访问的数据结构。 在设计并发数据结构时,你可以使用基本多线程应用中的构建块(之前章节中有提及),比如,互斥量和条件变量。当然,你也已经在之前的章节的例子中看到,怎样联合不同的构建块,对数据结构进行写入,并且保证这些构建块都是在并发环境下是线程安全的。 在本章,我们将了解一些并发数据结构设计的基本准则。然后,我们将再次重温锁和条件变量的基本构建块。最后,会去...
4.5-chinese
4.5 本章总结同步操作对于使用并发编写一款多线程应用来说,是很重要的一部分:如果没有同步,线程基本上就是独立的,也可写成单独的应用,因其任务之间的相关性,它们可作为一个群体直接执行。本章,我们讨论了各式各样的同步操作,从基本的条件变量,到“期望”、“承诺”,再到打包任务。我们也讨论了替代同步的解决方案:函数化模式编程,完全独立执行的函数,不会受到外部环境的影响;还有,消息传递模式,以消息子系统为中介,向线程异步的发送消息。 我们已经讨论了很多C++中的高层工具,现在我们来看一下底层工具是如何让一切都工作的:C++内存模型和原子操作。
6.1-chinese
6.1 为并发设计的意义何在?设计并发数据结构,意味着多个线程可以并发的访问这个数据结构,线程可对这个数据结构做相同或不同的操作,并且每一个线程都能在自己的自治域中看到该数据结构。且在多线程环境下,无数据丢失和损毁,所有的数据需要维持原样,且无条件竞争。这样的数据结构,称之为“线程安全”的数据结构。通常情况下,当多个线程对数据结构进行同一并发操作是安全的,但不同操作则需要单线程独立访问数据结构。或相反,当线程执行不同的操作时,对同一数据结构的并发操作是安全的,而多线程执行同样的操作,则会出现问题。 实际的设计意义并不止上面提到的那样:这就意味着,要为线程提供并发访问数据结构的机会。本质上,是使用互斥量提供互斥特性:在互斥量的保护下,同一时间内只有一个线程可以获取互斥锁。互斥量为了保护数据,显式的阻止了线程对数据结构的并发访问。 这被称为串行化(serialzation):线程轮流访问被保护的数据。这其实是对数据进行串行的访问,而非并发。因此,你需要对数据结构的设计进行仔细斟酌,确保其能真正并发访问。虽然,一些数据结构有着比其他数据结构多的并发访问范围,但是在所有情况下的思路都是一...
6.2-chinese
6.2 基于锁的并发数据结构基于锁的并发数据结构设计,需要确保访问线程持有锁的时间最短。对于只有一个互斥量的数据结构来说,这十分困难。需要保证数据不被锁之外的操作所访问到,并且还要保证不会在固有结构上产生条件竞争(如第3章所述)。当你使用多个互斥量来保护数据结构中不同的区域时,问题会暴露的更加明显,当操作需要获取多个互斥锁时,就有可能产生死锁。所以,在设计时,使用多个互斥量时需要格外小心。 在本节中,你将使用6.1.1节中的指导建议,来设计一些简单的数据结构——使用互斥量和锁的方式来保护数据。每一个例子中,都是在保证数据结构是线程安全的前提下,对数据结构并发访问的概率(机会)进行提高。 我们先来看看在第3章中栈的实现,这个实现就是一个十分简单的数据结构,它只使用了一个互斥量。但是,这个结构是线程安全的吗?它离真正的并发访问又有多远呢? 6.2.1 线程安全栈——使用锁我们先把第3章中线程安全的栈拿过来看看:(这里试图实现一个线程安全版的std:stack<>) 清单6.1 线程安全栈的类定义 12345678910111213141516171819202122232...
6.3-chinese
6.3 基于锁设计更加复杂的数据结构栈和队列都很简单:接口相对固定,并且它们应用于比较特殊的情况。并不是所有数据结构都像它们一样简单;大多数数据结构支持更加多样化的操作。原则上,这将增大并行的可能性,但是也让对数据保护变得更加困难,因为要考虑对所有能访问到的部分。当为了并发访问对数据结构进行设计时,这一系列原有的操作,就变得越发重要,需要重点处理。 先来看看,在查询表的设计中,所遇到的一些问题。 6.3.1 编写一个使用锁的线程安全查询表查询表或字典是一种类型的值(键值)和另一种类型的值进行关联(映射的方式)。一般情况下,这样的结构允许代码通过键值对相关的数据值进行查询。在C++标准库中,这种相关工具有:std::map<>, std::multimap<>, std::unordered_map<>以及std::unordered_multimap<>。 查询表的使用与栈和队列不同。栈和队列上,几乎每个操作都会对数据结构进行修改,不是添加一个元素,就是删除一个,而对于查询表来说,几乎不需要什么修改。清单3.13中有个例子,是一个简...
6.4-chinese
6.4 本章总结本章开篇,我们讨论了设计并发数据结构的意义,以及给出了一些指导意见。然后,通过设计一些通用的数据结构(栈,队列,哈希表和单链表),探究了在指导意见在实现这些数据结构的应用,并使用锁来保护数据和避免数据竞争。那么现在,你应该回看一下本章实现的那些数据结构,再回顾一下如何增加并发访问的几率,和哪里会存在潜在条件竞争。 在第7章中,我们将看一下如何避免锁完全锁定,使用底层原子操作来提供必要访问顺序约束,并给出一些指导意见。














