C++11其实主要就四方面内容,第一个是可变参数模板,第二个是右值引用,第三个是智能指针,第四个是内存模型(Memory Model)。 相对来说,这也是较难理解的几个特性,分别针对于泛型编程,内存优化,内存管理和并发编程。 并发编程是个非常大的模块,而在诸多内容底下有一个基本的概念,就是并发内存模型(Memory Model)。 那么,什么是内存模型? 1 Memory Model 早在之前介绍并发编程的文章中,我们就知道同步共享数据很重要。而同步可分为两种方式:原子操作和顺序约束。 原子操作是数据操作的最小单元,天生不可再分;顺序约束可以协调各个线程之间数据访问的先后顺序,避免数据竞争。 通常的同步方式会有两个问题,一是效率不够,二是死锁问题。导致效率不够是因为这些方式都是lock-based的。 当然,若非非常在意效率,完全可以使用这些同步方式,因其简单方便且不易出错。 若要追求更高的效率,需要学习lock-free(无锁)的同步方式。 内存模型,简单地说,是一种介于开发者和系统之间的并发约定,可以无锁地保证程序的执行逻辑与预期一致。 这里的系统包括编译器、处理器和缓存,各部分都想在自己的领域对程序进行优化,以提高性能,而这些优化会打乱源码中的执行顺序。尤其是在多线程上,这些优化会对共享数据造成巨大影响,导致程序的执行结果往往不遂人意。 内存模型,就是来解决这些优化所带来的问题。主要包含三个方面: Atomic operations(原子操作) Partial ordering of operations(局部执行顺序) Visible effects of operations(操作可见性) 原子操作和局部执行顺序如前所述,「操作可见性」指的是不同线程之间操作共享变量是可见的。 原子数据的同步是由编译器来保证的,而非原子数据需要我们自己来规划顺序。 2 关系定义 这里有三种关系术语, sequenced-before happens-before synchronizes-with 同一线程语句之间,若A操作在B操作之前执行,则表示为A sequenced-before B,A的执行结果对B可见。 而在不同线程的语句之间,若A操作在B操作之前就已发生,则表示为A happens-before B。该关系具有可传递性,也就是说,若A happens-before B,B happens-before C,则一定能得出A happens-before C。 若A操作的状态改变引发了B操作的执行,则表示为A synchronizes-with B。比如我们学过的事件、条件变量、信号量等等都会因一个条件(状态)满足,而执行相应的操作,这种状态关系就叫做synchronizes-with。 由于synchronizes-with的特性,可以借其实现happens-before关系。 内存模型就是提供一个操作的约束语义,借其可以满足上述关系,实现了顺序约束。 3 Atomics(原子操作) 原子操作的知识之前也介绍过,限于篇幅,便不再捉细节。 先来整体看一下原子操作支持的操作类型,后面再来讲应用。 这里挑两个来介绍一下相关操作,算是回顾。 第一个来讲atomic_flag,这是最简单的原子类型,代表一个布尔标志,可用它实现一个自旋锁: 1#include 2#include 3#include 4 5class spin_lock 6{ 7 std::atomic_flag flag = ATOMIC_FLAG_INIT; 8public: 9 void lock() { while(flag.test_and_set()); }1011 void unlock() { flag.clear(); }12};1314spin_lock spin;15int g_num = 0;16void work()17{18 spin.lock();1920 g_num++;2122 spin.unlock();23}2425int main()26{27 std::thread t1(work);28 std::thread t2(work);29 t1.join();30 t2.join();3132 std::cout << g_num;3334 return 0;35} atomic_flag必须使用ATOMIC_FLAG_INIT初始化,该值就是0,也就是false。 只能通过两个接口来操作atomic_flag: clear:清除操作,将值设为false。 test_and_set:将值设为true并返回之前的值。 第9行的lock()函数实现了自旋锁,当第一个线程进来的时候,由于atomic_flag为false,所以会通过test_and_set设置为true并返回false,第一个线程于是可以接着执行下面的逻辑。 当第二个线程进来时,flag为true,因此会一直循环,只有第一个线程中unlock了才会接着执行。由此保证了共享变量g_num。 第二个来讲atomic ,它所支持的原子操作要比atomic_flag多。 一个简单的同步操作: 1#include 2#include 3#include 4#include 5#include 6#include 7 8std::atomic<bool> flag{false}; 9std::vector<int> shared_values;10void work()11{12 std::cout << "waiting" << std::endl;13 while(!flag.load())14 {15 std::this_thread::sleep_for(std::chrono::milliseconds(5));16 }1718 shared_values[1] = 2;19 std::cout << "end of the work" << std::endl;20}2122void set_value()23{24 shared_values = { 7, 8, 9 };25 flag = true;26 std::cout << "data prepared" << std::endl;27}2829int main()30{31 std::thread t1(work);32 std::thread t2(set_value);33 t1.join();34 t2.join();3536 std::copy(shared_values.begin(), shared_values.end(), std::ostream_iterator<int>(std::cout, " "));3738 return 0;39} 这里有两个线程,它们之间拥有执行顺序,只有先在set_value函数中设置好共享值,才能在work函数中修改。 通过flag的load函数可以获取原子值,在值未设置完成时其为false,所以会一直等待数据到来。当flag变为true时,表示数据已经设置完成,于是会继续工作。 4 Memory ordering(内存顺序) 是什么保证了上述原子操作能够在多线程环境下同步执行呢? 其实在所有的原子操作函数中都有一个可选参数memory_order。比如atomic 的load()和store()原型如下: bool std::_Atomic_bool::load(std::memory_order _Order = std::memory_order_seq_cst) const noexceptvoid std::_Atomic_bool::store(bool _Value, std::memory_order _Order = std::memory_order_seq_cst) noexcept 这里的可选参数默认为memory_order_seq_cst,所有的memory_order可选值为: enum memory_order { memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst}; 这就是C++提供的如何实现顺序约束的方式,通过指定特定的memory_order,可以实现前面提及的sequence-before、happens-before、synchronizes-with关系。 顺序约束是我们和系统之间的一个约定,约定强度由强到弱可以分为三个层次: Sequential consistency(顺序一致性): memory_order_seq_cst Acquire-release(获取与释放): memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel Relaxed(松散模型): memory_order_relaxed Sequential consistency保证所有操作在线程之间都有一个全局的顺序,Acquire-release保证在不同线程间对于相同的原子变量的写和读的操作顺序,Relaxed仅保证原子的修改顺序。 为何要分层次呢? 其实顺序约束和系统优化之间是一种零和博弈,约束越强,系统所能够做的优化便越少。 因此每个层次拥有效率差异,层次越低,优化越多,效率也越高,不过掌握难度也越大。 所有的Memory order按照操作类型,又可分为三类: Read(读):memory_order_acquire,memory_order_consume Write(写):memory_order_release Read-modify-Write(读-改-写):memory_order_acq_rel,memory_order_seq_cst Relaxed未定义同步和顺序约束,所以要单独而论。 例如load()就是Read操作,store()就是Write()操作,compare_exchange_strong就是Read-modify-Write操作。 这意味着你不能将一个Read操作的顺序约束,写到store()上。例如,若将memory_order_acquire写到store()上,不会产生任何效果。 我们先来从默认的Sequential consistency开始,往往无需设置,便默认是memory_order_seq_cst,可以写一个简单的生产者-消费者函数: 1std::string sc_value;…