我读到的关于volatile的一切都说它永远不会安全,但我仍然倾向于尝试它,而且我还没有看到这个特定情况被宣布为不安全。
我有一个单独的线程渲染场景,从主模拟线程中提取数据。这没有同步,并且工作正常。
问题是当程序退出时,渲染器需要停止从模拟线程中提取数据,然后模拟线程才能安全地自行清理,而不会导致渲染器尝试读取无效内存。
为了实现这一点,我让渲染器在其线程中无限运行:
volatile bool stillRendering;
void RenderThreadFunction()
{
stillRendering = true;
while(programRunning)
{
renderer->render();
}
stillRendering = false;
}
在主程序线程中,当收到windproc退出消息时,我这样做:
void OnQuit()
{
programRunning = false;
while(stillRendering)
{
}
delete application;
}
这样做的目的是确保渲染器在应用程序上调用delete之前停止从应用程序中提取数据。
我首先尝试了这个没有任何volatile关键字,并且它在调试模式下工作,但在发布模式下它挂了。我假设编译器进行了一些优化,导致程序停止检查stillRendering的值。
将volatile添加到stillRendering会导致应用程序在我到目前为止每次测试时都成功退出。我不确定为什么“programRunning”不稳定似乎并不重要。
最后,我不确定如何使用volatile为“stillRendering”影响程序的性能。如果使StillRendering volatile影响OnQuit()的性能,对我来说无关紧要,但如果它影响RenderThreadFunction()的性能,那对我来说无关紧要
答案 0 :(得分:8)
它完全不安全,虽然它可能适用于某些人
编译器。基本上,volatile
仅影响它的变量
附加到,所以RendererThreadFunction
,例如,可以设置
在完成之前stillRendering
false
renderer->render();
。 (即使两者都是如此
stillRendering
和programRunning
都是不稳定的。)
问题的可能性非常小,所以测试很可能
不会透露它。最后,某些版本的VC ++ 做给出
volatile
C ++ 11下的原子访问的语义
在哪种情况下,您的代码将起作用。 (直到你编译
当然,不同版本的VC ++。)
鉴于renderer->render()
几乎可以肯定
一个不可忽视的时间,绝对没有理由
这里没有使用条件变量。关于唯一的时间
如果关闭,你会使用volatile
这类事情
机制由信号触发(在这种情况下,类型
将是sig_atomic_t
,而不是bool
,尽管在实践中,
它可能没有任何区别)。在那种情况下,那里
不会是两个线程,而只是渲染器线程和
信号处理程序。
答案 1 :(得分:6)
如果您希望您的代码适用于所有编译器中的所有体系结构,请使用C ++ 11 atomics:
std::atomic<bool> stillRendering;
void RenderThreadFunction()
{
stillRendering = true;
while(programRunning)
{
renderer->render();
}
stillRendering = false;
}
易失性不设计用于多线程 - 标准实际上允许编译器使用非volatile
访问重新排序volatile
次访问。 VC ++扩展了volatile
的功能集以防止重新排序,但其他编译器却没有,并且可能会破坏这些编译器。
正如其他人所提到的,volatile
也不会影响可见性,这意味着非缓存一致的体系结构可能永远不会看到标记集。 x86甚至不是立即缓存一致(写入速度非常慢),因此当通过各种缓冲区发送写入时,您的程序将不可避免地结束循环。
C ++ 11原子可以避免这两个问题。
好的,所以这主要是为了纠正您当前的代码并警告您不要滥用volatile
。詹姆斯建议使用条件变量(这只是你正在做的更有效的版本)可能是最适合你的实际解决方案。
答案 2 :(得分:4)
C ++ 11 atomics解决了三个问题。
首先,线程切换可以在读取或写入值的过程中发生;对于读取,另一个线程可以在原始线程读取其余值之前更新该值;对于写入,另一个线程可以看到半写值。这被称为“撕裂”。
其次,在典型的多处理器系统中,每个处理器都有自己的缓存,它会读取和写入该缓存的值;有时缓存和主内存会更新以确保它们保持相同的值,但是直到写入新值的处理器刷新其缓存并且读取该值的线程从缓存重新加载其副本时,值可能不同。这被称为“缓存一致性”。
第三,编译器可以移动代码,并在存储另一个值之前存储一个值,即使代码以相反的顺序写入。只要您不能编写可以看到差异的有效程序,那就可以在“似乎”规则下进行。
从原子变量加载并存储到原子变量(使用默认的内存排序)可以防止所有这三个问题。将变量标记为volatile
不会。
编辑:不要费心找出哪些架构会造成哪些问题。标准库的作者已经针对库实现所针对的体系结构执行了此操作。不要寻找捷径;只是使用原子。你不会失去任何东西。
答案 3 :(得分:2)
根据缓存一致的体系结构(例如x86处理器),我希望这可以正常工作。您可能会发现两个线程中的任何一个都可能比使用真正的原子操作更多地运行迭代,但由于只有一方设置而不是读取值,因此不需要真正的原子操作。
但是,如果正在执行代码的处理器(核心)需要特定的缓存刷新以使“其他核心”看到更新的值,那么您可能会被困一段时间 - 而且您需要正确原子更新以确保其他处理器的缓存无效。
我假设renderer->render()
需要相当长的时间,所以阅读stillRendering
不应该影响整个运行时间。 volatile
通常只是意味着“请不要把它放在寄存器中并保存在那里”。
(您可能还需要programRunning
成为volatile
!)
答案 4 :(得分:1)
将volatile添加到stillRendering会导致应用程序在每次测试时成功退出
是的,您的方案将有效。
使用volatile
变量进行线程同步时发生的常见错误是volatile
变量上的操作被假定为原子。他们不是。
在你的情况下,你正在轮询一个bool,等待它准确地改为一次。你似乎并不期望任何操作是原子的。另一方面,即使您正在轮询单个int
,C ++也不能保证更改该int的线程会以原子方式进行轮询。
我不确定为什么“programRunning”不稳定似乎并不重要。
这很重要。设为volatile
。
创建变量volatile
将保证避免特定的缓存优化,这就是您想要的。
这并不意味着当变量不是 volatile时,保证那些相同的缓存优化。你只是让编译器决定。在这个特定的时间,编译器正好做出适合你的决定。
最后,我不确定如何使用volatile来“StillRendering”来影响程序的性能。
您的表现可能会受到以下因素的负面影响:
while(stillRendering)
{
}
你要求一个线程(也许是一个整个CPU核心)无休止地,无需休息,读取一个变量。
考虑在while循环中添加一个sleep调用。