用于线程控制的volatile bool被认为是错误的吗?

时间:2011-08-09 11:17:52

标签: c++ multithreading multicore volatile

由于我对this question的回答,我开始阅读关键字volatile以及关于它的共识。我看到有很多关于它的信息,一些旧的现在看起来是错的,而且很多新的说它在多线程编程中几乎没有位置。因此,我想澄清具体的用法(在SO上找不到确切的答案)。

我还想指出,我确实理解编写多线程代码的要求以及为什么volatile没有解决问题。尽管如此,我仍然看到使用volatile进行代码库中的线程控制的代码。此外,这是我使用volatile关键字的唯一情况,因为所有其他共享资源都已正确同步。

假设我们有一个类:

class SomeWorker
{
public:
    SomeWorker() : isRunning_(false) {}
    void start() { isRunning_ = true; /* spawns thread and calls run */ }
    void stop() { isRunning_ = false; }

private:
    void run()
    {
        while (isRunning_)
        {
            // do something
        }
    }
    volatile bool isRunning_;
};

为简单起见,有些事情被遗漏了,但重要的是创建了一个对象,它在新生成的线程中检查(volatile)布尔值以确定它是否应该停止。只要希望工作者停止,就会从另一个线程设置此布尔值。

我的理解是在这种特定情况下使用volatile的原因只是为了避免任何将其缓存在循环寄存器中的优化。因此,导致无限循环。没有必要正确地同步事物,因为工作线程最终会获得新值?

我想了解这是否被认为是完全错误的,并且正确的方法是使用同步变量吗?编译器/架构/核心之间是否存在差异?也许这只是一个值得避免的草率方法?

如果有人澄清这一点,我会很高兴。谢谢!

修改

我有兴趣看到(在代码中)你如何选择解决这个问题。

5 个答案:

答案 0 :(得分:9)

volatile 可以用于此类目的。 然而这是标准C ++ by Microsoft的扩展名:

  

Microsoft特定

     

声明为volatile的对象是(...)

     
      
  • 对volatile对象的写入(volatile write)具有Release语义; (...)
  •   
  • 读取volatile对象(volatile read)具有Acquire语义; (...)
  •   
     

这允许在多线程应用程序中使用volatile对象进行内存锁定和释放。 (已添加)

也就是说,据我所知,当您使用Visual C ++编译器时,volatile bool最实用的目的是atomic<bool>

应该注意的是,newer VS版本添加了/volatile switch来控制此行为,因此仅在/volatile:ms处于活动状态时才会生效。

答案 1 :(得分:8)

您不需要 synchronized 变量,而是 atomic 变量。幸运的是,您可以使用std::atomic<bool>

关键问题是,如果多个线程同时访问同一个内存,那么除非访问是原子的,否则整个程序将不再处于定义良好的状态。也许你很幸运有一个bool,它可能在任何情况下都会以原子方式进行更新,但是在进攻中确定你做得对的唯一方法就是使用原子变量。

“查看你工作的代码库”在学习并发编程方面可能不是一个很好的衡量标准。并发编程非常困难,很少有人完全理解它,我愿意打赌绝大多数自制代码(即不使用专用的并发库)在某些方面是不正确的。问题是这些错误可能极难观察或重现,所以你可能永远不会知道。

编辑:你没有在你的问题中说如何 bool得到更新,所以我假设最坏的。例如,如果将整个更新操作包装在全局锁中,那么当然没有并发内存访问。

答案 2 :(得分:7)

多线程时遇到三个主要问题:

1)同步和线程安全。必须保护多个线程之间共享的变量不被多个线程一次写入,并防止在非原子写入期间被读取。对象的同步只能通过特殊的信号量/互斥对象来完成,该对象本身保证是原子的。 volatile关键字没有帮助。

2)指令管道。 CPU可以更改执行某些指令的顺序,以使代码运行得更快。在每个CPU执行一个线程的多CPU环境中,CPU管道指令而不知道系统中的另一个CPU正在执行相同操作。防止指令管道被称为记忆障碍。这一切都在Wikipedia得到了很好的解释。内存屏障可以通过专用的内存屏障对象或通过系统中的信号量/互斥对象来实现。当使用volatile关键字时,编译器可能会选择在代码中调用内存屏障,但这将是相当特殊的异常,而不是常态。我永远不会假设volatile关键字没有在编译器手册中验证过它。

3)编译器不知道回调函数。就硬件中断而言,一些编译器可能不知道在代码执行过程中已经执行了一个回调函数并更新了一个值。您可以使用以下代码:

// main
x=true;
while(something) 
{   
  if(x==true)   
  {
    do_something();
  }
  else
  {
    do_seomthing_else();
    /* The code may never go here: the compiler doesn't realize that x 
       was changed by the callback. Or worse, the compiler's optimizer 
       could decide to entirely remove this section from the program, as
       it thinks that x could never be false when the program comes here. */
  } 
}

// thread callback function:
void thread (void)
{
  x=false;
}

请注意,此问题仅出现在某些编译器上,具体取决于其优化程序设置。 volatile特征码解决了这个特殊问题。


所以问题的答案是:在一个多线程程序中,volatile关键字对线程同步/安全没有帮助,它可能不会起到内存屏障的作用,但它可以防止编译器的危险假设优化

答案 3 :(得分:6)

仅在单核上使用volatile就足够了,其中所有线程都使用相同的缓存。在多核上,如果在一个核上调用stop()而在另一个核上执行run(),则CPU缓存可能需要一些时间来同步,这意味着两个核可能会看到两个不同的{ {1}}。这意味着isRunning_在停止后会运行一段时间。

如果使用同步机制,它们将确保所有缓存获得相同的值,代价是暂停程序一段时间。性能或正确性对您来说更重要取决于您的实际需求。

答案 4 :(得分:0)

这适用于您的情况,但为了保护关键部分,这种方法是错误的。如果它是正确的,那么在几乎所有使用互斥锁的情况下都可以使用挥发性bool。原因是volatile变量不保证强制执行任何内存屏障,也不保证任何缓存一致性机制。相反,互斥量确实如此。换句话说,一旦锁定了互斥锁,就会向所有核心广播缓存失效,以便在所有核心之间保持一致性。对于volatile,情况并非如此。 然而,Andrei Alexandrescu proposed a very interesting approach使用volatile来强制执行共享对象的同步。正如你会看到他用互斥量做的那样; volatile仅用于防止在没有同步的情况下访问对象的接口。