非原子类型
C/C++中所有的操作默认为非原子操作。非原子操作,并不保证操作的完整性。当一个操作由2个以上的指令完成时,操作可能只执行了一个指令,变量就被另外一个线程抢占。当多个线程同时读写同一个变量时,就会发生数据竞争。
uint64_t i = 0;
void foo(){
i = 1;
}
上述简单的赋值语句,通过一个32位的系统编译,这个操作就需要分别负责高32位和低32位的两条指令来完成。对于非原子的这两条指令,任意线程都可以在它们期间去访问变量。
当一个线程抢占在这两个指令之间调用变量,变量只修改了低32位就被其他线程调用,此时会发生写撕裂。
读取情况与赋值情况类似,当一个线程抢占两个读取指令之间去修改变量,此时变量只读取了低32位,此时会发生读撕裂。
多个线程同时调用同一个变量,当其中一个线程执行写操作时,必须使用原子类型。
原子类型
原子类型的操作属于原子操作,对于它们的操作只有2种状态,完成和不做。不会出现操作进行一半而被其他线程抢占的情况。
#include <atomic>
#include <thread>
#include <assert.h>
bool x=false; // x现在是一个非原子变量
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
x=true; // 1 在栅栏前存储x
std::atomic_thread_fence(std::memory_order_release);
y.store(true,std::memory_order_relaxed); // 2 在栅栏后存储y
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed)); // 3 在#2写入前,持续等待
std::atomic_thread_fence(std::memory_order_acquire);
if(x) // 4 这里读取到的值,是#1中写入
++z;
}
int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load()!=0); // 5 断言将不会触发
}
上述代码,实现原子类型来对非原子类型操作排序的的功能。是否可以将原子变量y替换为非原子变量呢?答案是否定的。
对于变量y,线程a和线程b会同时进行访问,线程b不断读取等待线程a将y值修改。当变量y为非原子类型时,该代码就会发生数据竞争,引发“未定义行为”。
当使用原子类型进行操作排序时,“自由”内存顺序和非原子类型的操作顺序就等价了,可以将“自由”内存顺序的变量,直接替换为非原子类型。