线程间临界数据必须使用atomic类型而非普通内置类型吗?
我正在阅读《CPP-Concurrency-In-Action-2ed-2019》一书,在5.3章节中作者给出如下示例,用来讲解如何使用std::atomic<bool>实现线程间的代码序列控制(同步):
#include <vector> #include <atomic> #include <iostream> std::vector<int> data; std::atomic<bool> data_ready(false); void reader_thread() { while(!data_ready.load()) // 1 { std::this_thread::sleep(std::milliseconds(1)); } std::cout<<"The answer="<<data[0]<<"\n"; // 2 } void writer_thread() { data.push_back(42); // 3 data_ready=true; // 4 }
但我产生疑问:若用普通bool替代std::atomic<bool>,程序仍能保证线程安全吗?修改后的代码如下:
#include <vector> #include <atomic> #include <iostream> std::vector<int> data; bool data_ready(false); void reader_thread() { while(!data_ready) // 1 { std::this_thread::sleep(std::milliseconds(1)); } std::cout<<"The answer="<<data[0]<<"\n"; // 2 } void writer_thread() { data.push_back(42); // 3 data_ready=true; // 4 }
我知晓+=、-=这类复合操作并非线程安全,但仅对POD变量进行简单读写(如data_ready=true)本身是否就不安全?查阅相关问答后仍未能理解其中要点。
问题解答
普通bool无法保证线程安全,核心原因有两点:
缺乏内存可见性保障
writer线程修改普通bool变量data_ready后,reader线程可能永远无法看到这个修改。编译器会因为优化,将data_ready的值缓存到寄存器中,reader线程的循环会一直读取寄存器里的旧值,不会去同步内存中的新值;另外CPU缓存一致性机制也可能没有及时同步不同核心之间的缓存,导致reader核心看不到writer核心写入的新值。没有原子操作与happens-before关系的保障
原示例中的std::atomic<bool>操作会建立严格的happens-before关系:writer线程中data.push_back(42)(代码3)一定先于data_ready=true(代码4)执行,reader线程中data_ready的读取(代码1)一定先于data[0]的读取(代码2)执行,同时代码4的修改一定先于代码1的读取被观测到,最终保证reader线程读到的是已经初始化完成的data。
换成普通bool后,这个同步关系完全失效:编译器可能重排指令,把data_ready=true提前到data.push_back(42)之前执行;即使指令没重排,reader线程读到data_ready=true后,也可能因为缓存未同步,看到的data还是未初始化的状态,读取data[0]会触发未定义行为。
关于单字节POD读写的误区
很多人误以为单字节POD变量的读写是原子的,但C++标准仅保证原子类型的操作具备原子性,普通变量哪怕是单字节,也没有这个保证——不同CPU架构、编译器优化都可能打破原子性。而且就算原子性侥幸成立,内存可见性的问题依然存在,这才是导致同步失效的关键。
内容的提问来源于stack exchange,提问作者f1msch

