关于无锁数据类型的详细叙述,可以在无锁数据结构里面看,而内存顺序,则在无锁编程教程:简介 里面有讲。
在学习C++11的原子数据类型中,不免会遇到这样的语句——
std::atomic<bool> x; x.store(true,std::memory_order_seq_cst);
其中第一个参数很容易理解,但第二个参数就比较奇怪了。实际上,内存乱序是由于编译器和处理器为了提升单线程程序运行效率所引入的,而第二个参数就是尝试去告诉编译器和处理器,哪些地方千万不要自以为是的乱序。
从cplusplus.com上面可以看到更加详细的定义:
void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept; void store (T val, memory_order sync = memory_order_seq_cst) noexcept; T load (memory_order sync = memory_order_seq_cst) const volatile noexcept; T load (memory_order sync = memory_order_seq_cst) const noexcept; bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept; bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept; bool compare_exchange_weak (T& expected, T val, memory_order success, memory_order failure) volatile noexcept; bool compare_exchange_weak (T& expected, T val, memory_order success, memory_order failure) noexcept;
可以发现,基本所有涉及到加“锁”,放“锁”的地方,都会存在这样一个memory_order参数!
要理解到这个参数的意思,还得从C++编译器的优化说起。对于一个顺序执行的语句
int a = 1; int b = 2; a *= 2; b += 3;
看起来确实是按顺序执行,先修改a的值,再修改b的值。
但是我们可以发现第3行和第4行在cpu中的顺序可以完全交换,因为a,b内存地址是独立的,交换执行顺序并不会导致任何的错误。甚至,在大部分情况下,这两个语句的执行顺序交换或重叠可以使得程序跑的更快!
在摩尔定律几近失效的今天,当然不能放过任何的优化空间,处理器在执行代码时会按照自己的理解将这类独立的语句按照另一种顺序执行。对于单线程程序,完全没有问题。但是到了多线程里面,这样的交换顺序就不对了。
这是一个通过bool实现自旋锁的代码——
bool flag = false; while (!flag); flag = true; //TODO1 flag = false; //TODO2
不过这个程序第3行和第4行不是一个原子操作,也就是说其他线程可能在这个时候切入,导致数据访问错误。
而倘若将读取、判断、赋值合并为了一个操作。这样自旋锁就work了!这里用到了C++11的atomic<bool>类型
std::atomic<bool> flag = ATOMIC_VAR_INIT(false); //TODO 0 bool expected = false; while(!flag.compare_exchange_strong(expected, true, std::memory_order_acquire)) expected = false; //TODO1 flag.store(false, std::memory_order_release); //TODO2
一个小小的问题,之前谈到了编译器会按照自己的想法交换一些代码的位置,也就是说其他线程的TODO2的代码和TODO0的代码块都有可能在编译器的优化下越过我们的加锁位置跳到TODO1里面(只要没有严格先后次序的语句都是可以随便交换顺序的)。在多线程里面,这是一个致命的问题,这个优化导致了之前的努力全部泡汤了!
怎么办呢,别忘了我们还有memory_order参数——
- memory_order_acquire:执行该操作时,加入一个内存屏障,需要等待其他线程完成所有内存读
- memory_order_release:执行该操作时,加入一个内存屏障,需要等待本线程完成所有内存写
有了这两个操作,TODO1中的读写语句就严格和外部的语句隔离开了,潜在的风险也就没有了。
当然,memory_order不只这些,还包括
- memory_order_relaxed:完全不添加任何屏障
- memory_order_consume:同acquire,但是该屏障并不阻塞无关的读操作,只阻塞有依赖关系的读写(不知道如何做到的,比较神奇)
- memory_order_acq_rel:清空自己所在cpu的读写依赖
- memory_order_seq_cst:最严格的屏障,要求所有cpu的读写严格依赖
这些都是我自己从网上的博客中总结的,如果有什么不对的地方还请留言告诉我。
不过看起来挺靠谱的~v~
参考链接1:https://blog.poxiao.me/p/spinlock-implementation-in-cpp11/
参考链接2:http://blog.csdn.net/yockie/article/details/8838661
参考链接3:http://www.cplusplus.com/reference/atomic/memory_order/