动态调度场景下std::atomic应使用何种正确内存顺序?
原子计数器动态调度的正确内存顺序选择
问题场景
经典的"原子计数器动态调度"范式执行流程如下:
- 通过
fetch_add原子操作获取待处理元素的索引i - 若
i超出数组范围,线程终止执行 - 线程安全处理元素
i(由于计数器的原子性,其他线程不会获取到同一个i) - 回到第一步继续循环
示例代码
#include <atomic> std::atomic_int counter = 0; void foo(int *data, int size) { // 等价于counter++ for (int i; (i = counter.fetch_add(1, std::memory_order::seq_cst)) < size;) { data[i] *= 2; } }
驱动代码
#include <thread> #include <numeric> #include <cassert> int main() { int data[1'000'000]; std::iota(std::begin(data), std::end(data), 0); std::thread other{foo, data, std::size(data)}; foo(data, std::size(data)); other.join(); for (int i = 0; i < std::size(data); ++i) { assert(data[i] == i * 2); } }
原代码使用std::memory_order::seq_cst可以安全运行,但该内存顺序的约束过于严格,存在性能浪费。需要从以下选项中选择更合适的内存顺序:
std::memory_order::seq_cststd::memory_order::acq_relstd::memory_order::release
注:std::memory_order_relaxed和std::memory_order::acquire过于宽松,无法保证data[0] *= 2这类操作不会被重排到第一次fetch_add之前,因此不在可选范围内。
正确答案:std::memory_order::acq_rel
详细分析
acq_rel的适配性fetch_add属于读-修改-写(RMW)原子操作,使用acq_rel内存顺序时:
- 获取(acquire)语义:会阻止处理器将后续的元素处理操作(如
data[i] *= 2)重排到fetch_add之前,彻底避免了"未拿到合法索引就修改数组"的错误情况。 - 释放(release)语义:确保当前线程对
counter的修改(即fetch_add的递增操作)对其他线程可见,同时阻止之前的元素处理操作被重排到fetch_add之后——不过由于每个索引i仅被一个线程处理,这一点在本场景中更多是保证原子操作的可见性一致性,而非避免数据竞争。
- 其他选项的问题
std::memory_order::seq_cst:虽然安全,但它会强制所有原子操作在全局范围内形成一个统一的总顺序,带来不必要的性能开销。本场景不需要全局顺序,仅需保证线程内操作顺序和计数器的原子性即可。std::memory_order::release:仅提供释放语义,没有获取语义。处理器可能会将后续的数组修改操作重排到fetch_add之前,导致线程在未拿到合法索引时就修改数组元素,破坏调度逻辑的安全性。
内容的提问来源于stack exchange,提问作者Jan Schultke
相关产品推荐
相关产品推荐

