ATM:自动取款机。1回到第4章,我举了一个使用消息传递框架在线程间发送信息的例子。这里就会使用这个实现来完成ATM功能。下面完整代码就是功能的实现,包括消息传递框架。清单C.1实现了一个消息队列。其可以将消息以指针(指向基类)的方式存储在列表中;指定消息类型会由基类派生模板进行处理。推送包装类的构造实例,以及存储指向这个实例的指针;
虽然,C++11才开始正式支持并发,不过,高级编程语言都支持并发和多线程已经不是什么新鲜事了。例如,Java在第一个发布版本中就支持多线程编程,在某些平台上也提供符合POSIX C标准的多线程接口,还有Erlang支持消息的同步传递(有点类似于MPI)。
本附录仅是摘录了部分C++11标准的新特性,因为这些特性和线程库之间有着良好的互动。其他的新特性,包括:静态断言(static_assert),强类型枚举(enum class),委托构造函数,Unicode码支持,模板别名,以及统一的初始化序列。对于新功能的详细描述已经超出了本书的范围;需要另外一本书来进行详细介绍。
线程本地变量允许程序中的每个线程都有一个独立的实例拷贝。可以使用thread_local关键字来对这样的变量进行声明。命名空间内的变量,静态成员变量,以及本地变量都可以声明成线程本地变量,为了在线程运行前对这些数据进行存储操作:thread_local int x; // 命名空间内的线程本地变量class X{ static thread_local std::string s; // 线程本地的静态成员变量};
C++是静态语言:所有变量的类型,都会在编译时被准确指定。所以,作为程序员你需要为每个变量指定对应的类型。有些时候就需要使用一些繁琐类型定义,比如:std::map<std::string,std::unique_ptr<some_data>> m;std::map<std::string,std::unique_ptr<some_data>>::iterator iter=m.find("my key");
变参模板:就是可以使用不定数量的参数进行特化的模板。就像你接触到的变参函数一样,printf就接受可变参数。现在,就可以给你的模板指定不定数量的参数了。变参模板在整个C++线程库中都有使用,例如:std::thread的构造函数就是一个变参类模板。从使用者的角度看,仅知道模板可以接受无限个参数就够了,不过当要写这么一个模板或对其工作原理很感兴趣时,就需要了解一些细节。
lambda函数在C++11中的加入很是令人兴奋,因为lambda函数能够大大简化代码复杂度(语法糖:利于理解具体的功能),避免实现调用对象。C++11的lambda函数语法允许在需要使用的时候进行定义。能为等待函数,例如std::condition_variable(如同4.1.1节中的例子)提供很好谓词函数,其语义可以用来快速的表示可访问的变量,而非使用类中函数来对成员变量进行捕获。
整型字面值,例如42,就是常量表达式。所以,简单的数学表达式,例如,23x2-4。可以使用其来初始化const整型变量,然后将const整型变量作为新表达的一部分:const int i=23;const int two_i=i*2;const int four=4;const int forty_two=two_i-four;使用常量表达式创建变量也可用在其他常量表达式中,有些事只能用常量表达式去做:指定数组长度:int bounds=99;
删除函数的函数可以不进行实现,默认函数就则不同:编译器会创建函数实现,通常都是“默认”实现。当然,这些函数可以直接使用(它们都会自动生成):默认构造函数,析构函数,拷贝构造函数,移动构造函数,拷贝赋值操作符和移动赋值操作符。为什么要这样做呢?
有时让类去做拷贝是没有意义的。std::mutex就是一个例子——拷贝一个互斥量,意义何在?std::unique_lock<>是另一个例子——一个实例只能拥有一个锁;如果要复制,拷贝的那个实例也能获取相同的锁,这样std::unique_lock<>就没有存在的意义了。实例中转移所有权(A.1.2节)是有意义的,其并不是使用的拷贝。当然其他例子就不一一列举了。
如果你从事过C++编程,你会对引用比较熟悉,C++的引用允许你为已经存在的对象创建一个新的名字。对新引用所做的访问和修改操作,都会影响它的原型。例如:int var=42;int& ref=var; // 创建一个var的引用ref=99;assert(var==99); // 原型的值被改变了,因为引用被赋值了目前为止,我们用过的所有引用都是左值引用——对左值的引用。
新的C++标准,不仅带来了对并发的支持,也将其他语言的一些特性带入标准库中。在本附录中,会给出对这些新特性进行简要介绍(这些特性用在线程库中)。除了thread_local(详见A.8部分)以外,就没有与并发直接相关的内容了,但对于多线程代码来说,它们都是很重要。我已只列出有必要的部分(例如,右值引用),这样能够使代码更容易理解。
本章我们了解了各种与并发相关的bug,从死锁和活锁,再到数据竞争和其他恶性条件竞争;我们也使用了一些技术来定位bug。同样,也讨论了在做代码审阅的时候需做哪些思考,以及写可测试代码的指导意见,还有如何为并发代码构造测试用例。最终,我们还了解了一些对测试很有帮助的工具。
之前的章节,我们了解了与并发相关的错误类型,以及如何在代码中体现出来的。这些信息可以帮助我们来判断,的代码中是否存在有隐藏的错误。最简单直接的就是直接看代码。虽然看起来比较明显,但是要彻底的修复问题,却是很难的。读刚写完的代码,要比读已经存在的代码容易的多。
你可以在并发代码中发现各式各样的错误,这些错误不会集中于某个方面。不过,有一些错误与使用并发直接相关,本章重点关注这些错误。通常,并发相关的错误通常有两大类:不必要阻塞条件竞争这两大类的颗粒度很大,让我们将其分成颗粒度较小的问题。10.1.1 不必要阻塞“不必要阻塞”是什么意思?
本章主要内容并发相关的错误定位错误和代码审查设计多线程测试用例多线程代码的性能目前为止,我们了解如何写并发代码——可以使用哪些工具,这些工具应该如何使用。不过,在软件开发中重要的一部分我们还没有提及:测试与调试。如果你希望阅读完本章后就能很轻松的去调试并发代码,本章无法满足你的预期。测试和调试并发代码比较麻烦。
在本章中,我们了解各种“高级”线程管理技术:线程池和中断线程。也了解了如何使用本地任务队列,使用任务窃取的方式减小同步开销,提高线程池的吞吐量;等待子任务完成的同时执行队列中其他任务,从而来避免死锁。也了解了使用线程去中断另一个处理线程的各种方式,比如:使用特定的断点和函数执行中断,要不就是使用某种方法,对阻塞等待进行中断。
很多情况下,使用信号来终止一个长时间运行的线程是合理的。这种线程的存在,可能是因为工作线程所在的线程池被销毁,或是用户显式的取消了这个任务,亦或其他各种原因。不管是什么原因,原理都一样:需要使用信号来让未结束线程停止运行。这里需要一种合适的方式让线程主动的停下来,而非让线程戛然而止。你可能会给每种情况制定一个独立的机制,这样做的意义不大。
很多公司里,雇员通常会在办公室度过他们的办公时光(偶尔也会外出访问客户或供应商),或是参加贸易展会。虽然外出可能很有必要,并且可能需要很多人一起去,不过对于一些特别的雇员来说,一趟可能就是几个月,甚至是几年。公司要给每个雇员都配一辆车,这基本上是不可能的,不过公司可以提供一些共用车辆;这样就会有一定数量车,来让所有雇员使用。
关注时代Java