C++多线程实践
C++多线程实践
C++内存模型和原子类型操作
std::memory_order初探
动态内存模型可以理解为存储一致性模型,主要是从行为上来看多个线程对同一个对象读写操作时所做的约束,动态内存理解起来会有少许复杂,涉及到内存、Cache、CPU的各个层次的交互。
如下有两个线程,分别对a、R1、b、R2进行赋值,根据线程执行的顺序可能有以下几种情况
在不对线程进行任何限制,线程内部指令不进行重排的情况下。可以有4!/(2!*2!)=6中情况
在不考虑优化和指令重排的情况下,多线程有如下两种情况:
  1. 程序最终执行的结果,是多个线程交织执行的结果
  1. 从单个线程来看,该线程的指令是按照事先已经规定好的执行顺序执行
当然,现在的编译器都支持指令重排,上述的现象也只是理想情况下的执行情况,因为顺序一致性代价太大不利于程序的优化。但是有时候你需要对编译器的优化行为做出一定的约束,才能保证你的程序行为和你预期的执行结果保持一致,那么这个约束就是内存模型。
C++程序员想要写出高性能的多线程程序必须理解内存模型,编译器会给你的程序做静态优化,CPU为了提升性能也有动态乱序执行的行为。总之,实际编程中程序不会完全按照你原始代码的顺序来执行,因此内存模型就是程序员、编译器、CPU之间的契约。编程、编译、执行都会在遵守这个契约的情况下进行,在这样的规则之上各自做自己的优化,从而提升程序的性能。
Memory Order
内存的顺序描述了计算机CPU获取内存的顺序,内存的排序可能静态也可能动态的发生:
  • 静态内存排序:编译器期间,编译器对内存重排
  • 动态内存排序:运行期间,CPU乱序执行
静态内存排序是为了提高代码的利用率和性能,编译器对代码进行了重新排序;同样为了优化性能CPU也会进行对指令进行重新排序、延缓执行、各种缓存等等,以便达到更好的执行效果。虽然经过排序确实会导致很多执行顺序和源码中不一致,但是你没有必要为这些事情感到棘手足无措。任何的内存排序都不会违背代码本身所要表达的意义,并且在单线程的情况下通常不会有任何的问题。
但是在多线程场景中,无锁的数据结构设计中,指令的乱序执行会造成无法预测的行为。所以我们通常引入内存栅栏这一概念来解决可能存在的并发问题。
Memory Barrier
内存栅栏是一个令 CPU 或编译器在内存操作上限制内存操作顺序的指令,通常意味着在 barrier 之前的指令一定在 barrier 之后的指令之前执行。
在 C11/C++11 中,引入了六种不同的 memory order,可以让程序员在并发编程中根据自己需求尽可能降低同步的粒度,以获得更好的程序性能。这六种 order 分别是:
C++11中规定了如下6种访问次序(Memory Oreder)