2.3-chinese
2.3 转移线程所有权假设要写一个在后台启动线程的函数,想通过新线程返回的所有权去调用这个函数,而不是等待线程结束再去调用;或完全与之相反的想法:创建一个线程,并在函数中转移所有权,都必须要等待线程结束。总之,新线程的所有权都需要转移。 这就是移动引入std::thread的原因,C++标准库中有很多_资源占有_(resource-owning)类型,比如std::ifstream,std::unique_ptr还有std::thread都是可移动,但不可拷贝。这就说明执行线程的所有权可以在std::thread实例中移动,下面将展示一个例子。例子中,创建了两个执行线程,并且在std::thread实例之间(t1,t2和t3)转移所有权: 12345678void some_function();void some_other_function();std::thread t1(some_function); // 1std::thread t2=std::move(t1); // 2t1=std::thread(some_other_...
2.4-chinese
2.4 运行时决定线程数量std::thread::hardware_concurrency()在新版C++标准库中是一个很有用的函数。这个函数将返回能同时并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这也无法掩盖这个函数对启动线程数量的帮助。 清单2.8实现了一个并行版的std::accumulate。代码中将整体工作拆分成小任务交给每个线程去做,其中设置最小任务数,是为了避免产生太多的线程。程序可能会在操作数量为0的时候抛出异常。比如,std::thread构造函数无法启动一个执行线程,就会抛出一个异常。在这个算法中讨论异常处理,已经超出现阶段的讨论范围,这个问题我们将在第8章中再来讨论。 清单2.8 原生并行版的std::accumulate 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849template<typename Iterator,...
2.5-chinese
2.5 标识线程线程标识类型是std::thread::id,可以通过两种方式进行检索。第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值,这个值表示“无线程”。第二种,当前线程中调用std::this_thread::get_id()(这个函数定义在<thread>头文件中)也可以获得线程标识。 std::thread::id对象可以自由的拷贝和对比,因为标识符就可以复用。如果两个对象的std::thread::id相等,那它们就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。 线程库不会限制你去检查线程标识是否一样,std::thread::id类型对象提供相当丰富的对比操作;比如,提供为不同的值进行排序。这意味着允许程序员将其当做为容器的键值,做排序,或做其他方式的比较。按默认顺序比较不同值的std::thread::id,所以这个行为可预见的:当a<b...
2.6-chinese
2.6 本章总结本章讨论了C++标准库中基本的线程管理方式:启动线程,等待结束和不等待结束(因为需要它们运行在后台)。并了解应该如何在线程启动前,向线程函数中传递参数,如何转移线程的所有权,如何使用线程组来分割任务。最后,讨论了使用线程标识来确定关联数据,以及特殊线程的特殊解决方案。虽然,现在已经可以纯粹的依赖线程,使用独立的数据,做独立的任务(如同清单2.8),但在某些情况下,线程确实需要有共享数据。第3章会讨论共享数据和线程的直接关系。第4章会讨论在(有/没有)共享数据情况下的线程同步操作。
3.0-chinese
第3章 线程间共享数据本章主要内容 共享数据带来的问题 使用互斥量保护数据 数据保护的替代方案 上一章中,我们已经对线程管理有所了解了,现在让我们来看一下“共享数据的那些事”。 想象一下,你和你的朋友合租一个公寓,公寓中只有一个厨房和一个卫生间。当你的朋友在卫生间时,你就会不能使用了(除非你们特别好,好到可以在同时使用一个房间)。这个问题也会出现在厨房,假如:厨房里有一个组合式烤箱,当在烤香肠的时候,也在做蛋糕,就可能得到我们不想要的食物(香肠味的蛋糕)。此外,在公共空间将一件事做到一半时,发现某些需要的东西被别人借走,或是当离开的一段时间内有些东西被变动了地方,这都会令我们不爽。 同样的问题,也困扰着线程。当线程在访问共享数据的时候,必须定一些规矩,用来限定线程可访问的数据位。还有,一个线程更新了共享数据,需要对其他线程进行通知。从易用性的角度,同一进程中的多个线程进行数据共享,有利有弊。错误的共享数据使用是产生并发bug的一个主要原因,并且后果要比香肠味的蛋糕更加严重。 本章就以在C++中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时,将共享数据的优势发挥到...
3.1-chinese
3.1 共享数据带来的问题当涉及到共享数据时,问题很可能是因为共享数据修改所导致。如果共享数据是只读的,那么只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多麻烦。这种情况下,就必须小心谨慎,才能确保一切所有线程都工作正常。 不变量(invariants)的概念对程序员们编写的程序会有一定的帮助——对于特殊结构体的描述;比如,“变量包含列表中的项数”。不变量通常会在一次更新中被破坏,特别是比较复杂的数据结构,或者一次更新就要改动很大的数据结构。 双链表中每个节点都有一个指针指向列表中下一个节点,还有一个指针指向前一个节点。其中不变量就是节点A中指向“下一个”节点B的指针,还有前向指针。为了从列表中删除一个节点,其两边节点的指针都需要更新。当其中一边更新完成时,不变量就被破坏了,直到另一边也完成更新;在两边都完成更新后,不变量就又稳定了。 从一个列表中删除一个节点的步骤如下(如图3.1) 找到要删除的节点N 更新前一个节点指向N的指针,让这个指针指向N的下一个节点 更新后一个节点指向N的指针,让这个...
3.3-chinese
3.3 保护共享数据的替代设施互斥量是最通用的机制,但其并非保护共享数据的唯一方式。这里有很多替代方式可以在特定情况下,提供更加合适的保护。 一个特别极端(但十分常见)的情况就是,共享数据在并发访问和初始化时(都需要保护),但是之后需要进行隐式同步。这可能是因为数据作为只读方式创建,所以没有同步问题;或者因为必要的保护作为对数据操作的一部分,所以隐式的执行。任何情况下,数据初始化后锁住一个互斥量,纯粹是为了保护其初始化过程(这是没有必要的),并且这会给性能带来不必要的冲击。出于以上的原因,C++标准提供了一种纯粹保护共享数据初始化过程的机制。 3.3.1 保护共享数据的初始化过程假设你与一个共享源,构建代价很昂贵,可能它会打开一个数据库连接或分配出很多的内存。 延迟初始化(Lazy initialization)在单线程代码很常见——每一个操作都需要先对源进行检查,为了了解数据是否被初始化,然后在其使用前决定,数据是否需要初始化: 123456789std::shared_ptr<some_resource> resource_ptr;void foo(){...
3.4-chinese
3.4 本章总结本章讨论了当两个线程间的共享数据发生恶性条件竞争会带来多么严重的灾难,还讨论了如何使用std::mutex,和如何避免这些问题。如你所见,互斥量并不是灵丹妙药,其还有自己的问题(比如:死锁),虽然C++标准库提供了一类工具来避免这些(例如:std::lock())。你还见识了一些用于避免死锁的先进技术,之后了解了锁所有权的转移,以及一些围绕如何选取适当粒度锁产生的问题。最后,讨论了在具体情况下,数据保护的替代方案,例如:std::call_once()和boost::shared_mutex。 还有一个方面没有涉及到,那就是等待其他线程作为输入的情况。我们的线程安全栈,仅是在栈为空时,抛出一个异常,所以当一个线程要等待其他线程向栈压入一个值时(这是一个线程安全栈的主要用途之一),它不得不多次尝试去弹出一个值,当捕获抛出的异常时,再次进行尝试。这种消耗资源的检查,没有任何意义。并且,不断的检查会影响系统中其他线程的运行,这反而会妨碍程序的进展。我们需要一些方法让一个线程等待其他线程完成任务,但在等待过程中不占用CPU。第4章中,会去建立一些工具,用于保护共享数据,还...
3.2-chinese
3.2 使用互斥量保护共享数据当程序中有共享数据,肯定不想让其陷入条件竞争,或是不变量被破坏。那么,将所有访问共享数据结构的代码都标记为互斥岂不是更好?这样任何一个线程在执行这些代码时,其他任何线程试图访问共享数据结构,就必须等到那一段代码执行结束。于是,一个线程就不可能会看到被破坏的不变量,除非它本身就是修改共享数据的线程。 当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。线程库需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据,而不破坏不变量。 互斥量是C++中一种最通用的数据保护机制,但它不是“银弹”;精心组织代码来保护正确的数据(见3.2.2节),并在接口内部避免竞争条件(见3.2.3节)是非常重要的。但互斥量自身也有问题,也会造成死锁(见3.2.4节),或是对数据保护的太多(或太少)(见3.2.8节)。 3.2.1 C++中使用互斥量C++中通过实例化std::mutex创建互斥量,通过调用成员函数lock()进行上锁,unloc...
4.0-chinese
第4章 同步并发操作本章主要内容 等待事件 带有期望的等待一次性事件 在限定时间内等待 使用同步操作简化代码 在上一章中,我们看到各种在线程间保护共享数据的方法。当你不仅想要保护数据,还想对单独的线程进行同步。例如,在第一个线程完成前,可能需要等待另一个线程执行完成。通常情况下,线程会等待一个特定事件的发生,或者等待某一条件达成(为true)。这可能需要定期检查“任务完成”标识,或将类似的东西放到共享数据中,但这与理想情况还是差很多。像这种情况就需要在线程中进行同步,C++标准库提供了一些工具可用于同步操作,形式上表现为条件变量(condition variables)和期望(futures)。 在本章,将讨论如何使用条件变量等待事件,以及介绍期望,和如何使用它简化同步操作。













