英文:
Is it safe to non-atomically update a value selected by relaxed atomic operation without any synchronization?
问题
以下是翻译好的内容:
结构体 Values {
原子整数 counter;
整数 needed_counter;
// 我们会更新这个值。
一些类型 unsynchronized_value;
}
函数 更新数组(Values[] values) {
对于(Values& val : values) {
// 假设溢出是不可能的。
如果 (val.counter.fetch_add(1, relaxed) != val.needed_counter) { // 条件
继续;
}
// 对于CPU来说这是无操作,但可以防止编译器优化
// 将其前后的操作重新排列。
编译器屏障(acquire_release);
更新未同步的值(&mut val.unsynchronized_value);
}
// 将当前线程中的所有更改一次性刷新。
内存屏障(release);
}
请注意,代码中的注释也已经被翻译。
英文:
Example pseudocode:
struct Values{
AtomicInt counter;
Int needed_counter;
// We update this.
SomeType unsynchronized_value;
}
fn update_array(Values[] values){
for(Values& val : values) {
// Let's assume that overflow is impossible.
if (val.counter.fetch_add(1, relaxed) != val.needed_counter) { // CONDITION
continue;
}
// This is noop for CPU but prevents compiler optimizations
// from moving operations around it.
compiler_fence(acquire_release);
UpdateUnsynchonisedValue(&mut val.unsynchronized_value);
}
// Flush all changes made in current thread at once.
memory_fence(release);
}
On the one hand, since CONDITION
can be false only for one thread so it seems that no synchronization is needed.
On the other hand, modern CPUs do out of order and speculative execution so I wonder if it can cause data race by starting executing UpdateUnsynchonisedValue
function before checking CONDITION
.
Since I put compiler_fence
, compiler should not reorder instructions in UpdateUnsynchonisedValue before checking CONDITION
so question only about CPUs behaviour and memory model of languages like C++ or Rust.
答案1
得分: 3
这段代码在C++中是安全的,不需要额外的compiler_fence
。
fetch_add(1, relaxed)
将为每个线程产生唯一的索引,因为即使使用std::memory_order::relaxed
,也总是需要使用原子的最新值来进行读取-修改-写入(RMW)操作。
UpdateUnsynchonisedValue(&mut val.unsynchronized_value);
不能在val.counter.fetch_add(...)
之前重新排序,因为根据获取的值,控制流可能不会达到此函数调用。如果这种重新排序是可能的,您也无法编写相同的单线程代码。这将是不合理的。
通常情况下,如果代码依赖于条件,那么代码不能在条件之前重新排序。
推测执行可能导致在分支被执行之前执行代码,但这不会改变程序指令的效果。如果推测性内存写入能够引发数据竞争,那么编译器将负责插入一个屏障,以防止这种情况发生。实际上,这是不必要的,因为推测写入是缓冲的,并且如果分支未被执行,那么在写入到全局内存之前,它们将被丢弃。
请记住:您编写的代码是针对C++抽象机器的,在这台机器上,只有在进入if语句时,val.unsynchronized_value
才会被写入。无论实现细节如何,编译器都必须生成好像是这种情况的代码。
英文:
This code is safe in C++ without the additional compiler_fence
.
fetch_add(1, relaxed)
will yield a unique index for each thread, because read-modify-write (RMW) operations are always required to use the latest value of an atomic, even with std::memory_order::relaxed
.
UpdateUnsynchonisedValue(&mut val.unsynchronized_value);
cannot be reordered before val.counter.fetch_add(...)
, because control flow might not reach this function call depending on the fetched value. If this reordering was possible, you wouldn't be able to write the same single-threaded code either. It would be insane.
In general, code cannot be reordered before a condition, if the code depends on the condition.
Speculative execution can result in code being executed before a branch has been taken, but this cannot change the effect of the program's instructions. If a speculative memory write was able to cause a data race, then it would be the responsibility of the compiler to insert a fence which prevents this. In practice, this isn't necessary, because speculate writes are buffered, and will be discarded if a branch isn't taken, before anything is written to global memory.
Remember: the code you write is targeting the C++ abstract machine, and on this machine, val.unsynchronized_value
won't be written to if the if-statement isn't entered. Compilers must emit code that behaves as if this was the case, regardless of implementation details like speculative execution.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论