我已经阅读了很多有关'volatile'关键字的内容,但是我仍然没有明确的答案。
考虑以下代码:
class A
{
public:
void work()
{
working = true;
while(working)
{
processSomeJob();
}
}
void stopWorking() // Can be called from another thread
{
working = false;
}
private:
bool working;
}
work()进入其循环时,“ working”的值为true。
现在,我猜测编译器可以将 while(working)优化为 while(true),因为'working'的值为true启动循环时。
for(int i = 0; i < someOtherClassMember; i++)
{
doSomething();
}
...因为每次迭代都必须加载someOtherClassMember的值。
这两个是哪一个?谷歌搜索 volatile 的使用时,我发现人们声称它仅在与直接写入内存的I / O设备一起使用时才有用,但是我也发现它应该在像我这样的场景中使用。>
答案 0 :(得分:2)
您的程序将优化为无限循环†。
void foo() { A{}.work(); }
被编译为(带有O2的g ++)
foo():
sub rsp, 8
.L2:
call processSomeJob()
jmp .L2
该标准定义了假设的抽象机对程序的处理方式。符合标准的编译器必须编译您的程序,使其在所有可观察的行为中均与该计算机具有相同的行为。这就是所谓的 as 规则,只要您的程序执行的 是相同的,无论如何,编译器都有自由。
通常,对变量的读写并不构成可观察性,这就是为什么编译器可以根据需要进行尽可能多的读写操作。编译器可以看到working
未被分配,并优化了读取。 volatile
的(通常被误解)效果恰恰是使它们可观察,这迫使编译器将读取和写入操作留为单独操作‡。
但是,请您说,另一个线程可能会分配给working
。这就是出现未定义行为的余地的地方。当出现未定义行为(包括格式化硬盘并仍然符合标准)时,编译器可能会执行任何操作。由于没有同步并且working
不是原子的,因此任何其他写入working
的线程都是数据竞争,这是无条件的未定义行为。因此,无限循环唯一的错误是出现未定义的行为,编译器据此决定您的程序也应该继续循环。
TL; DR请勿将普通bool
和volatile
用于多线程。使用std::atomic<bool>
。
†并非在所有情况下都如此。 void bar(A& a) { a.work(); }
不适用于某些版本。
‡实际上,这周围有一些debate。
答案 1 :(得分:2)
现在我猜是允许编译器将while(working)优化为while(true)
可能是。但是只有当它可以证明processSomeJob()
不会修改working
变量时,即可以证明循环是无限的。
如果不是这种情况,那将意味着这样的效率将非常低下……因为someOtherClassMember的值必须在每次迭代时都加载
您的推理是正确的。但是,内存位置可能仍保留在缓存中,并且从CPU缓存中读取数据并不一定会显着降低速度。如果doSomething
的复杂程度足以导致someOtherClassMember
被从缓存中逐出,那么请确保我们必须从内存中进行加载,但是另一方面,doSomething
可能是如此复杂,以至于相比而言,单个内存的负载微不足道。
这两个是哪个?
要么。优化器将无法分析所有可能的代码路径;我们不能假设循环可以在所有情况下都得到优化。但是,如果证明someOtherClassMember
没有在任何代码路径中进行修改,那么从理论上证明这是可能的,因此可以从理论上优化循环。
但是我也发现,在我的情况下应该使用[volatile]。
volatile
在这里无济于事。如果working
在另一个线程中被修改,则存在数据争用。数据竞争意味着程序的行为是不确定的。
为避免数据争用,您需要进行同步:使用互斥锁或原子操作在线程之间共享访问。
答案 2 :(得分:1)
Volatile
将使while循环在每次检查时重新加载working
变量。实际上,这通常允许您通过从异步信号处理程序或其他线程进行的stopWorking
调用来停止工作功能,但按照标准,这还不够。该标准要求无锁原子或类型为volatile sig_atomic_t
的变量用于sighandler <->常规上下文通信,以及 atomics 用于线程间通信。