不承担同步责任的原子变量用relaxed,只要保证计数原子就行了 ,承担同步责任的看用啥:
1.生产者消费者模型采用acquire-release
2.或者用acq_rel只是耗性能
举例:
``
void callback()
{
// 准备数据
prepared_data[thread_id] = gather_data();
// 计数:relaxed足够
int tmp = data_count.fetch_add(1,
std::memory_order_relaxed);
// 处理第4次时进行同步,这个判断把同步责任转移到了原子变量a身上
// 同步:release
if (tmp == 3)
{
a.store(true, std::memory_order_release);
}
}
void thread1()
{
// 重置:relaxed足够
a.store(false, std::memory_order_relaxed);
data_count.store(0, std::memory_order_relaxed);
//调用异步接口,把这两个原子变量扔给多线程回调处理
asyncHandle(a, data_count, callback);//伪代码,异步接口立即返回
// 等待:acquire必须(与release配对)
while (!a.load(std::memory_order_acquire))
{
sleep(5ms);
}
// 安全使用prepared_data
use(prepared_data);
}
``
可以看到用到了relaxed和acquire、release,该场景如下说明:
(1)线程1先启动 ,把各个原子变量赋初值------这个时候没有竞争,所以relaxed就足够了。然后扔给多线程回调处理(把回调函数callback注册给多线程处理,触发多线程回调),接着等待处理完成标志------这个时候涉及到多线程竞争了,线程1是消费者线程,同步责任的原子变量只有a,所以a在消费者这块用load+acquire。
(2)callback是多线程回调处理函数,其内部是处理4次数据后置完成标志位。而同步责任的原子变量只有a,所以a在生产者这块用store+release。在这个回调里呢,计数这个原子变量不涉及同步,因为加了if把同步责任转移给原子变量a了,所以呢,这个计数原子变量只要保证++是原子的就行,所以用了宽松的内存序检查relaxed。
什么叫同步责任?
在生产者消费者模型下,生产者可能多个生产者(多个线程)来准备数据,准备好数据后(判断条件和业务有关系),这些数据要被消费者感知并处理,假设准备好数据后要置标志位,那这个标志位变量就承担了同步责任。
什么叫内存序?
保证代码的执行顺序,不同的内存序编译器编译出来的代码不一样,有的可能和你写个代码的前后顺序不一样。
比如
std::atomic ready{false};
int data = 0;
// 线程1(生产者)
data = 42; // 步骤1:准备数据
ready = true; // 步骤2:设置标志
// 线程2(消费者)
while (!ready){ // 步骤3:检查标志
// 等待
}
cout << data; // 步骤4:读取数据
在没有内存序保证的情况下,步骤1和步骤2可能被重排!
什么是"重排"?编译器重排编译器为了优化,可能把代码变成:
// 编译器优化后(错误!)
ready = true; // 步骤2先执行!
data = 42; // 步骤1后执行
后果:消费者看到不一致的状态
时间线:
线程1:ready = true → data = 42 // 重排了!
线程2:看到ready=true → 读取data=0 // 读到旧数据!
6种内存序,
设计原理:
正交组合
6种内存序实际上是2个维度的组合:
维度1:操作类型(3种)
加载操作(读) :需要acquire语义
存储操作(写) :需要release语义
读-修改-写操作(RMW):需要两者兼备
维度2:同步强度(2种强度 + 1个特殊)
弱同步 :relaxed(无同步)
强同步 :seq_cst(默认,完全同步)
中间强度:consume/acquire/release/acq_rel(精确控制)