是否存在互斥量有助于摆脱易变的关键词?

时间:2009-10-23 21:48:10

标签: c++ multithreading compiler-construction mutex volatile

我有一个多R / W锁定类,用于保持读取,写入和挂起读取,待处理写入计数器。互斥锁可以防御多个线程。

我的问题是我们是否仍然需要将计数器声明为volatile,以便编译器在进行优化时不会搞砸它。

或者编译器是否考虑到计数器由互斥锁保护。

我理解互斥锁是用于同步的运行时机制,“volatile”关键字是编译器在执行优化时执行正确操作的编译时指示。

此致 -Jay。

5 个答案:

答案 0 :(得分:15)

这里有两个基本上不相关的项目,总是很混乱。

  • 易失性
  • 线程,锁,内存障碍等。

volatile用于告诉编译器生成代码以从内存中读取变量,而不是从寄存器中读取。并且不重新排序代码。一般而言,不要优化或采取“捷径”。

内存障碍(由互斥锁,锁等提供),如Herb Sutter在另一个答案中引用的那样,是为了防止 CPU 重新排序读/写内存请求,无论编译器如何说去做吧。即不要优化,不要采取捷径 - 在CPU级别。

类似,但实际上是非常不同的事情。

在你的情况下,并且在大多数情况下锁定,不需要volatile的原因是因为为了锁定而进行了函数调用。即:

影响优化的正常函数调用:

external void library_func(); // from some external library

global int x;

int f()
{
   x = 2;
   library_func();
   return x; // x is reloaded because it may have changed
}

除非编译器可以检查library_func()并确定它不接触x,否则它将在返回时重新读取x。这甚至没有波动。

线程:

int f(SomeObject & obj)
{
   int temp1;
   int temp2;
   int temp3;

   int temp1 = obj.x;

   lock(obj.mutex); // really should use RAII
      temp2 = obj.x;
      temp3 = obj.x;
   unlock(obj.mutex);

   return temp;
}

在读取temp1的obj.x之后,编译器将重新读取temp2的obj.x - 不是因为锁的魔力 - 而是因为它不确定lock()是否修改了obj。你可以设置编译器标志来积极地优化(no-alias等),因此不会重新读取x,但是你的一堆代码可能会开始失败。

对于temp3,编译器(希望)不会重新读取obj.x. 如果由于某种原因,obj.x可能会在temp2和temp3之间发生变化,那么你会使用volatile(并且你的锁定会被破坏/无用)。

最后,如果你的lock()/ unlock()函数以某种方式内联,那么编译器可能会评估代码并看到obj.x没有被更改。但我保证这里有两件事情之一:    - 内联代码最终调用一些操作系统级锁定功能(从而阻止评估)或    - 您调用一些asm内存屏障指令(即包含在内联函数中,如__InterlockedCompareExchange),编译器将识别这些指令,从而避免重新排序。

编辑:P.S。我忘了提到 - 对于pthreads的东西,一些编译器被标记为“POSIX兼容”,这意味着,除其他外,他们将识别pthread_函数,而不是围绕它们进行错误的优化。即使C ++标准尚未提及线程,那些编译器也会(至少是最低限度)。

所以,简短回答

你不需要挥发性。

答案 1 :(得分:13)

来自Herb Sutter的文章“使用关键部分(最好是锁定)来消除种族”(http://www.ddj.com/cpp/201804238):

  

因此,要使重新排序转换有效,它必须遵守关键部分的一个关键规则来尊重程序的关键部分:代码不能移出关键部分。 (代码移入总是可以的。)我们通过要求任何关键部分的开头和结尾的对称单向栅栏语义来强制执行这条黄金法则,如图1中的箭头所示:

     
      
  • 输入关键部分是获取操作或隐式获取栅栏:代码永远不会越过栅栏,即从栅栏后的原始位置移动到栅栏之前执行。但是,在源代码顺序中出现在围栏之前的代码可以愉快地向下跨越围栏以便稍后执行。
  •   
  • 退出临界区是一个释放操作,或者是一个隐式释放栏:这只是反向要求,即代码不能向下跨越围栏,只能向上跨越。它保证看到最终版本写入的任何其他线程也会看到它之前的所有写入。
  •   

因此,对于编译器为目标平台生成正确的代码,当输入和退出临界区时(并且术语临界区用于它的一般意义,不一定是Win32意义上的受{{{{ 1}}结构 - 临界区可以被其他同步对象保护)必须遵循正确的获取和释放语义。因此,只要在受保护的关键部分中访问它们,就不必将共享变量标记为volatile。

答案 2 :(得分:5)

volatile用于通知优化器始终加载位置的当前值,而不是将其加载到寄存器中并假设它不会更改。当使用双端口存储器位置或可以从线程外部源实时更新的位置时,这是最有价值的。

互斥体是一种运行时操作系统机制,编译器实际上并不知道 - 因此优化器不会考虑到这一点。它会阻止多个线程同时访问计数器,但即使互斥锁生效,这些计数器的值仍然会发生变化。

所以,你要标记vars是不稳定的,因为它们可以被外部修改,而不是因为它们在互斥锁中。

让它们变得不稳定。

答案 3 :(得分:4)

虽然这可能取决于您使用的线程库,但我的理解是任何体面的库都需要使用volatile

在Pthreads for example中,使用互斥锁将确保您的数据正确地提交到内存。

编辑:我特此赞同tony's answer比我自己更好。

答案 4 :(得分:3)

您仍然需要“volatile”关键字。

互斥锁阻止计数器进行并发访问。

“volatile”告诉编译器实际使用计数器 而不是将其缓存到CPU寄存器中(不会 由并发线程更新。