我有一个多读/写锁类,用于保留读、写和挂起读、挂起写计数器。互斥体保护它们免受多个线程的影响。

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

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

据我所知,互斥锁是一种用于同步的运行时机制,“易失性”关键字是编译时指示编译器在进行优化时做正确的事情。

问候 - 杰伊。

有帮助吗?

解决方案

这里有两个基本不相关的项目,总是很容易混淆。

  • 易挥发的
  • 线程、锁、内存屏障等。

挥发性用于告诉 编译器 生成从内存而不是寄存器读取变量的代码。并且不要对代码进行重新排序。一般来说,不要优化或走“捷径”。

正如 Herb Sutter 在另一个答案中引用的那样,内存屏障(由互斥锁、锁等提供)是为了防止 中央处理器 无论编译器如何指示,都不会重新排序读/写内存请求。即不要在CPU 级别进行优化,不要走捷径。

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

在您的情况下,以及在大多数锁定情况下,不需要 volatile 的原因是因为 函数调用 是为了锁定而制作的。IE:

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

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。您可能可以设置编译器标志来积极优化(无别名等),从而不重新读取 x,但随后您的一堆代码可能会开始失败。

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

最后,如果您的 lock()/unlock() 函数以某种方式内联,也许编译器可以评估代码并看到 obj.x 没有改变。但我在这里保证两件事之一:- 内联代码最终调用某些OS级锁定功能(从而阻止评估)或 - 您调用一些ASM内存屏障指令(即包裹在__Interlockedcompareexchange之类的内联函数中),您的编译器会识别并避免重新排序。

编辑:附:我忘了提及 - 对于 pthreads 的东西,一些编译器被标记为“POSIX 兼容”,这意味着,除其他外,它们将识别 pthread_ 函数,并且不会对其进行不良优化。即,尽管 C++ 标准尚未提及线程,但这些编译器却提及了(至少是最低限度)。

所以,简短的回答

你不需要易失性。

其他提示

来自 Herb Sutter 的文章“使用临界区(最好是锁)来消除竞争”(http://www.ddj.com/cpp/201804238):

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

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

因此,为了让编译器为目标平台生成正确的代码,当进入和退出临界区时(术语临界区是在一般意义上使用的,不一定是受 Win32 保护的东西) CRITICAL_SECTION 结构 - 关键部分可以由其他同步对象保护)必须遵循正确的获取和释放语义。因此,只要共享变量仅在受保护的临界区中访问,您就不必将它们标记为易失性。

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

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

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

让它们变得不稳定。

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

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

编辑:我特此认可托尼的回答比我自己更好。

你仍然需要“挥发性”。关键字。

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

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top