9

我读到的关于 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 添加到 just stillRendering 会导致应用程序在我到目前为止每次测试时都成功退出。我不确定为什么“programRunning”是否不稳定似乎并不重要。

最后,我不确定将 volatile 用于“stillRendering”会如何影响程序的性能。使 stillRendering volatile 影响 OnQuit() 的性能对我来说无关紧要,但如果它影响 RenderThreadFunction() 的性能对我来说却很重要

4

5 回答 5

8

尽管它可能适用于某些编译器,但它是完全不安全的。基本上,volatile只影响它附加到的变量,因此RendererThreadFunction,例如,可以 在完成 之前stillRendering设置false 。(即使两者都是易变的也是如此 。)问题的概率非常小,因此测试可能不会揭示它。最后,某些版本的 VC++确实在 C++11 下提供 了原子访问的语义,在这种情况下,您的代码将可以工作。(当然,除非您使用不同版本的 VC++ 进行编译。)renderer->render();stillRenderingprogramRunningvolatile

鉴于renderer->render()几乎可以肯定会花费不可忽略的时间,因此绝对没有理由不在这里使用条件变量。大约唯一一次volatile用于此类事情的是关闭机制是否由信号触发(在这种情况下,类型将是sig_atomic_t,而不是bool,尽管在实践中,它可能没有任何区别)。在这种情况下,不会有两个线程,而只有渲染器线程和一个信号处理程序。

于 2013-02-25T19:28:48.867 回答
6

如果您希望您的代码适用于所有编译器中的所有架构,请使用 C++11 atomics:

std::atomic<bool> stillRendering;

void RenderThreadFunction()
{
    stillRendering = true;

    while(programRunning)
    {
        renderer->render();
    }

    stillRendering = false;
}

Volatile不是为多线程而设计的——标准实际上允许编译器将volatile访问与非volatile访问重新排序。VC++ 扩展volatile了 's 的功能集以防止重新排序,但其他编译器没有,它可能会在这些编译器上中断。

正如其他人所提到的,volatile它也不会影响可见性,这意味着不缓存一致的架构可能永远不会看到标志集。x86 甚至不是立即缓存一致的(写入会非常慢),因此当写入通过各种缓冲区发送时,您的程序总是会循环超过应有的次数。

C++11 atomics 避免了这两个问题。

好的,所以这主要是为了更正您当前的代码并警告您不要滥用volatile. 詹姆斯关于使用条件变量的建议(这只是你正在做的更有效的版本)可能是你最好的实际解决方案。

于 2013-02-25T19:24:14.220 回答
4

C++11 atomics 解决了三个问题。

首先,线程切换可能发生在读取或写入值的中间;对于读取,另一个线程可以在原始线程读取其余值之前更新该值;对于写入,另一个线程可以看到一半写入的值。这被称为“撕裂”。

其次,在典型的多处理器系统中,每个处理器都有自己的缓存,并且它会在该缓存中读取和写入值;有时会更新缓存和主内存以确保它们保持相同的值,但是直到写入新值的处理器刷新其缓存并且读取该值的线程从缓存中重新加载其副本,该值可能会有所不同。这被称为“缓存一致性”。

Third, the compiler can move code around, and store one value before it stores another, even if the code is written in the opposite order. As long as you can't write a valid program that can see the difference, that's okay under the "as if" rule.

Loading from and storing to an atomic variable (with the default memory-ordering) prevents all three problems. Marking a variable as volatile does not.

EDIT: don't bother figuring out which architectures pose which problems. The author of the standard library has already done this for the architecture that the library implementation is intended for. Don't look for shortcuts; just use atomics. You won't lose anything.

于 2013-02-25T20:45:02.683 回答
2

根据缓存一致的架构(例如 x86 处理器),我希望这可以正常工作。您可能会发现,与使用真正的原子操作相比,您的两个线程中的任何一个都可能运行更多的迭代,但由于只有一侧是设置值与读取值,因此不需要真正的原子操作。

但是,如果正在执行代码的处理器(核心)需要特定的缓存刷新以使“其他核心”看到更新的值,那么您可能会被卡住一段时间 - 您需要适当的原子更新才能确保其他处理器的缓存无效。

我假设这renderer->render()需要相当多的时间,所以阅读stillRendering应该不会对整体运行时间产生太大影响。volatile通常只是意味着“请不要将其放入寄存器并保留在那里”。

(你可能也需要programRunning成为一个volatile!)

于 2013-02-25T18:53:36.857 回答
2

将 volatile 添加到 just stillRendering 会导致应用程序在我每次测试时成功退出

是的,您的方案将起作用。

使用volatile变量进行线程同步时发生的常见错误是对volatile变量的操作被假定为原子操作。他们不是。

在您的情况下,您正在轮询一个布尔值,等待它恰好一次更改为 0。您似乎并不期望任何操作是原子的。另一方面,即使您轮询单个int,C++ 也不能保证更改该 int 的线程会原子地这样做。

我不确定为什么“programRunning”是否不稳定似乎并不重要。

这很重要。 让它volatile

制作变量volatile将保证避免特定的缓存优化,这是您想要的。

这并不意味着当变量不是易失性时,您可以保证获得相同的缓存优化。你只是让编译器决定。在这个特定的时间,编译器恰好正在做出适合您的决定。

最后,我不确定将 volatile 用于“stillRendering”会如何影响程序的性能。

您的表现可能会受到以下负面影响:

while(stillRendering)
{
}

您要求一个线程(可能是一个完整的 CPU 内核)无休止地读取单个变量。

考虑在该 while 循环中添加睡眠调用。

于 2013-02-25T19:03:13.790 回答