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