简体   繁体   English

std :: atomic应该是不稳定的吗?

[英]Should std::atomic be volatile?

I'm running a thread that runs until a flag is set. 我正在运行一个运行的线程,直到设置了一个标志。

std::atomic<bool> stop(false);

void f() {
  while(!stop.load(std::memory_order_{relaxed,acquire})) {
    do_the_job();
  }
}

I wonder if the compiler can unroll loop like this (I don't want it to happen). 我想知道编译器是否可以像这样展开循环(我不希望它发生)。

void f() {
  while(!stop.load(std::memory_order_{relaxed,acquire})) {
    do_the_job();
    do_the_job();
    do_the_job();
    do_the_job();
    ... // unroll as many as the compiler wants
  }
}

It is said that volatility and atomicity are orthogonal, but I'm a bit confused. 据说波动率和原子性是正交的,但我有点困惑。 Is the compiler free to cache the value of the atomic variable and unroll the loop? 编译器是否可以自由缓存原子变量的值并展开循环? If the compiler can unroll the loop, then I think I have to put volatile to the flag, and I want to be sure. 如果编译器可以展开循环,那么我认为我必须将volatile放到标志中,我想确定。

Should I put volatile ? 我应该把volatile


I'm sorry for being ambiguous. 我很抱歉暧昧。 I (guess that I) understand what reordering is and what memory_order_* s mean, and I'm sure I fully understand what volatile is. 我(我猜我)理解重新排序是什么以及memory_order_*是什么意思,我确信我完全理解volatile是什么。

I think the while() loop can be transformed as an infinite if statements like this. 我认为while()循环可以转换为这样的无限if语句。

void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  do_the_job();
  ...
}

Since the given memory orders don't prevent the sequenced-before operations from being moved past the atomic load, I think it can be rearranged if it's without volatile. 由于给定的内存顺序不会阻止序列化之前的操作被移动超过原子载荷,我认为如果它没有易失性,它可以重新排列。

void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  if(stop.load(std::memory_order_{relaxed,acquire})) return;
  ...
  do_the_job();
  do_the_job();
  do_the_job();
  ...
}

If the atomic does not imply volatile, then I think the code can be even transformed like this at worst case. 如果原子并不意味着易失性,那么我认为在最坏的情况下代码甚至可以像这样转换。

void f() {
  if(stop.load(std::memory_order_{relaxed,acquire})) return;

  while(true) {
    do_the_job();
  }
}

There will never be such an insane implementation, but I guess it's still a possible situation. 永远不会有这样疯狂的实施,但我想这仍然是一个可能的情况。 I think the only way to prevent this is to put volatile to the atomic variable and am asking about it. 我认为防止这种情况的唯一方法是将volatile放到原子变量上并询问它。

There are a lot of guesses that I made, please tell me if there's anything wrong among them. 我做了很多猜测,请告诉我他们之间是否有什么问题。

Is the compiler free to cache the value of the atomic variable and unroll the loop? 编译器是否可以自由缓存原子变量的值并展开循环?

The compiler cannot cache the value of an atomic variable. 编译器无法缓存原子变量的值。

However, since you are using std::memory_order_relaxed , that means the compiler is free to reorder loads and stores from/to this atomic variable with regards to other loads and stores. 但是,由于您使用的是std::memory_order_relaxed ,这意味着编译器可以自由地对此原子变量的加载和存储进行重新排序,以及其他加载和存储。

Also note, that a call to a function whose definition is not available in this translation unit is a compiler memory barrier. 另请注意,对此转换单元中定义不可用的函数的调用是编译器内存屏障。 That means the the call cannot not be reordered with regards to surrounding loads and stores and that all non-local variables must be reloaded from memory after the call, as if they were all marked volatile. 这意味着调用不能对周围的加载和存储进行重新排序,并且所有非局部变量必须在调用后从内存中重新加载,就好像它们都标记为volatile一样。 (Local variables whose address was not passed elsewhere will not be reloaded though). (不会重新加载其地址未在其他地方传递的局部变量)。

The transformation of code you would like to avoid would not be a valid transformation because that would violate C++ memory model: in the first case you have one load of an atomic variable followed by a call to do_the_job , in the second, you have multiple calls. 您希望避免的代码转换不是有效的转换,因为这会违反C ++内存模型:在第一种情况下,您有一个原子变量的加载,然后调用do_the_job ,在第二种情况下,您有多个调用。 The observed behaviour of the transformed code may be different. 观察到的变换代码的行为可能不同。


And a note from std::memory_order : 来自std :: memory_order的注释

Relationship with volatile 与挥发性的关系

Within a thread of execution, accesses (reads and writes) to all volatile objects are guaranteed to not be reordered relative to each other, but this order is not guaranteed to be observed by another thread, since volatile access does not establish inter-thread synchronization. 在执行的一个线程中,对所有易失性对象的访问(读取和写入)保证不会相对于彼此重新排序,但是这个顺序不能保证被另一个线程观察到,因为易失性访问不会建立线程间同步。

In addition, volatile accesses are not atomic (concurrent read and write is a data race) and do not order memory ( non-volatile memory accesses may be freely reordered around the volatile access ). 此外,易失性访问不是原子的(并发读和写是数据竞争)并且不命令存储器( 非易失性存储器访问可以在易失性访问周围自由重新排序 )。

This bit non-volatile memory accesses may be freely reordered around the volatile access is true for relaxed atomics as well, since relaxed load and stores can be reordered with regards to other loads and stores. 这种位非易失性存储器访问可以在易失性访问周围自由地重新排序,对于轻松原子也是如此,因为可以关于其他加载和存储重新排序松弛的加载和存储。

In other words, adorning your atomic with volatile would not change the behaviour of your code. 换句话说,用volatile来装饰你的原子不会改变代码的行为。


Regardless, C++11 atomic variables do not need to be marked with volatile keyword. 无论如何,C ++ 11原子变量不需要用volatile关键字标记。


Here is an example how g++-5.2 honours atomic variables. 以下是g ++ - 5.2如何表达原子变量的示例。 The following functions: 以下功能:

__attribute__((noinline)) int f(std::atomic<int>& a) {
    return a.load(std::memory_order_relaxed);
}

__attribute__((noinline)) int g(std::atomic<int>& a) {
    static_cast<void>(a.load(std::memory_order_relaxed));
    static_cast<void>(a.load(std::memory_order_relaxed));
    static_cast<void>(a.load(std::memory_order_relaxed));
    return a.load(std::memory_order_relaxed);
}

__attribute__((noinline)) int h(std::atomic<int>& a) {
    while(a.load(std::memory_order_relaxed))
        ;
    return 0;
}

Compiled with g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S编译g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S produce the following assembly: g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S生成以下程序集:

f(std::atomic<int>&):
    movl    (%rdi), %eax
    ret

g(std::atomic<int>&):
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    movl    (%rdi), %eax
    ret

h(std::atomic<int>&):
.L4:
    movl    (%rdi), %eax
    testl   %eax, %eax
    jne .L4
    ret

If do_the_job() does not change stop , it doesn't matter if the compiler can unroll the loop, or not. 如果do_the_job()没有改变stop ,那么编译器是否可以展开循环并不重要。

std::memory_order_relaxed just makes sure each operation is atomic, but it does not prevent reordering accesses. std::memory_order_relaxed只是确保每个操作都是原子操作,但它不会阻止重新排序访问。 That means if another thread sets stop to true , the loop may continue to execute a few times, because the accesses may be reordered. 这意味着如果另一个线程将stop设置为true ,则循环可以继续执行几次,因为可以重新排序访问。 So it is the same situation as with an unrolled loop: do_the_job() may be executed a few times after another thread has set stop to true . 所以它与展开循环的情况相同: do_the_job()可能在另一个线程将stop设置为true后执行几次。

So no, don't use volatile , use std::memory_order_acquire and std::memory_order_release . 所以不,不要使用volatile ,使用std::memory_order_acquirestd::memory_order_release

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM