原子操作的内存序、happens-before关系及代码相关技术咨询
示例代码
static int zero = 0; static int proc_enter(struct proc_context* ctx, struct lkf_node* node) { __atomic_store_n(&node->next, NULL, __ATOMIC_RELAXED); // 0 __atomic_store_n(&ctx->list.tail, &node->next, __ATOMIC_RELEASE); // 1 int n = (int) __atomic_compare_exchange_n(&ctx->stat, &zero, 1, false, __ATOMIC_RELEASE, // 2 __ATOMIC_RELAXED); return n - 1; } static int proc_leave(struct proc_context* ctx) { __atomic_store_n(&ctx->stat, 0, __ATOMIC_RELAXED); // 3 __atomic_thread_fence(__ATOMIC_ACQ_REL); // 4 if (__atomic_load_n(&ctx->list.tail, __ATOMIC_RELAXED) == &ctx->list.root.next) { // 5 return 0; } int n = (int) __atomic_compare_exchange_n(&ctx->stat, &zero, 1, false, __ATOMIC_RELAXED, __ATOMIC_RELAXED); return n - 1; }
问题解答
1. 是否需要__ATOMIC_RELEASE内存序来保证//0操作happens before//1操作?
不需要。同一线程内的操作天然遵循程序顺序(Program Order),不管原子操作的内存序是什么,当前线程里//0的执行必然早于//1,且//0的结果对//1可见。__ATOMIC_RELAXED已经足够,不需要额外的RELEASE内存序来保证线程内的happens-before关系。
2. __atomic_compare_exchange_n中的__ATOMIC_RELEASE是否是保证//1操作happens before//2操作所必需的?
不需要。和上面的逻辑一样,同一线程内的操作天然有程序顺序的happens-before约束,//1的写操作必然早于//2的CAS操作,且结果对//2可见。这里的RELEASE内存序是用来和其他线程的ACQUIRE操作建立跨线程同步的,不是为了保证线程内的执行顺序。
3. 是否需要__ATOMIC_ACQ_REL内存序来保证//5操作happens before//3操作?
首先要纠正逻辑:代码里//3是先执行的写操作(写入ctx->stat为0),//5是后执行的读操作(读取ctx->list.tail),你实际需要保证的是**//3的写操作完成后再执行//5的读操作**,也就是阻止CPU将//5的读重排到//3的写之前。
当前的__ATOMIC_ACQ_REL栅栏确实能实现这个效果:它会阻止栅栏之前的存储操作(//3)被重排到栅栏之后,同时阻止栅栏之后的加载操作(//5)被重排到栅栏之前。但注意,需求不是//5 happens before //3,而是反过来,__ATOMIC_ACQ_REL是用来保证这个正向顺序的。
4. 如果__atomic_thread_fence能保证//3操作happens before//5操作,那么它是否是实现该约束的唯一方式?
不是唯一方式,可以通过给原子操作本身指定内存序来替代独立栅栏:
- 方案一:将//3的
__ATOMIC_RELAXED改为__ATOMIC_RELEASE,同时将//5的__ATOMIC_RELAXED改为__ATOMIC_ACQUIRE。RELEASE写会强制自身及之前的存储操作完成,且阻止后续操作重排到它前面;ACQUIRE读会强制自身及之后的加载操作不被重排到前面,两者结合同样能保证//3的写在//5的读之前执行,不会被CPU重排。 - 方案二:仅将//3的内存序改为
__ATOMIC_RELEASE。RELEASE写本身会强制之前的存储提交到内存,并且阻止后续的加载操作重排到它前面,这样//5的读也不会跑到//3的写之前。
结合你提到的CPU加载-存储队列逻辑:
- 写入
ctx->stat(//3) - 刷新存储队列:对应RELEASE写或栅栏的存储屏障,确保之前的写操作都提交到内存,不会"下沉"到后续操作之后
- 刷新加载队列:对应ACQUIRE读或栅栏的加载屏障,确保后续的读操作不会被"提升"到前面操作之前
- 加载
ctx->list.tail(//5)
用原子操作的内存序替代独立栅栏,本质是把屏障逻辑绑定到具体的原子操作上,效果和独立栅栏完全一致,不需要额外的栅栏指令。
内容的提问来源于stack exchange,提问作者user2256177

