互斥锁的存在是否有助于摆脱 volatile 关键字?

发布于 2024-08-09 07:42:29 字数 210 浏览 8 评论 0原文

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

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

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

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

问候, -杰伊。

I have a multi-R/W lock class that keeps the read, write and pending read , pending write counters. A mutex guards them from multiple threads.

My question is Do we still need the counters to be declared as volatile so that the compiler won't screw it up while doing the optimization.

Or does the compiler takes into account that the counters are guarded by the mutex.

I understand that the mutex is a run time mechanism to for synchronization and "volatile" keyword is a compile time indication to the compiler to do the right thing while doing the optimizations.

Regards,
-Jay.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

野稚 2024-08-16 07:42:29

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

  • 易失性
  • 线程、锁、内存屏障等。

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

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

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

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

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

所以,简短的回答

你不需要不稳定。

There are 2 basically unrelated items here, that are always confused.

  • volatile
  • threads, locks, memory barriers, etc.

volatile is used to tell the compiler to produce code to read the variable from memory, not from a register. And to not reorder the code around. In general, not to optimize or take 'short-cuts'.

memory barriers (supplied by mutexes, locks, etc), as quoted from Herb Sutter in another answer, are for preventing the CPU from reordering read/write memory requests, regardless of how the compiler said to do it. ie don't optimize, don't take short cuts - at the CPU level.

Similar, but in fact very different things.

In your case, and in most cases of locking, the reason that volatile is NOT necessary, is because of function calls being made for the sake of locking. ie:

Normal function calls affecting optimizations:

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
}

unless the compiler can examine library_func() and determine that it doesn't touch x, it will re-read x on the return. This is even WITHOUT volatile.

Threading:

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;
}

After reading obj.x for temp1, the compiler is going to re-read obj.x for temp2 - NOT because of the magic of locks - but because it is unsure whether lock() modified obj. You could probably set compiler flags to aggressively optimize (no-alias, etc) and thus not re-read x, but then a bunch of your code would probably start failing.

For temp3, the compiler (hopefully) won't re-read obj.x.
If for some reason obj.x could change between temp2 and temp3, then you would use volatile (and your locking would be broken/useless).

Lastly, if your lock()/unlock() functions were somehow inlined, maybe the compiler could evaluate the code and see that obj.x doesn't get changed. But I guarantee one of two things here:
- the inline code eventually calls some OS level lock function (thus preventing evaluation) or
- you call some asm memory barrier instructions (ie that are wrapped in inline functions like __InterlockedCompareExchange) that your compiler will recognize and thus avoid reordering.

EDIT: P.S. I forgot to mention - for pthreads stuff, some compilers are marked as "POSIX compliant" which means, among other things, that they will recognize the pthread_ functions and not do bad optimizations around them. ie even though the C++ standard doesn't mention threads yet, those compilers do (at least minimally).

So, short answer

you don't need volatile.

同尘 2024-08-16 07:42:29

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

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

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

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

From Herb Sutter's article "Use Critical Sections (Preferably Locks) to Eliminate Races" (http://www.ddj.com/cpp/201804238):

So, for a reordering transformation to be valid, it must respect the program's critical sections by obeying the one key rule of critical sections: Code can't move out of a critical section. (It's always okay for code to move in.) We enforce this golden rule by requiring symmetric one-way fence semantics for the beginning and end of any critical section, illustrated by the arrows in Figure 1:

  • Entering a critical section is an acquire operation, or an implicit acquire fence: Code can never cross the fence upward, that is, move from an original location after the fence to execute before the fence. Code that appears before the fence in source code order, however, can happily cross the fence downward to execute later.
  • Exiting a critical section is a release operation, or an implicit release fence: This is just the inverse requirement that code can't cross the fence downward, only upward. It guarantees that any other thread that sees the final release write will also see all of the writes before it.

So for a compiler to produce correct code for a target platform, when a critical section is entered and exited (and the term critical section is used in it's generic sense, not necessarily in the Win32 sense of something protected by a CRITICAL_SECTION structure - the critical section can be protected by other synchronization objects) the correct acquire and release semantics must be followed. So you should not have to mark the shared variables as volatile as long as they are accessed only within protected critical sections.

乱世争霸 2024-08-16 07:42:29

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

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

因此,您将变量标记为易失性,因为它们可以在外部修改,而不是因为它们位于互斥锁内部。

保持它们的波动性。

volatile is used to inform the optimizer to always load the current value of the location, rather than load it into a register and assume that it won't change. This is most valuable when working with dual-ported memory locations or locations that can be updated real-time from sources external to the thread.

The mutex is a run-time OS mechanism that the compiler really doesn't know anything about - so the optimizer wouldn't take that into account. It will prevent more than one thread from accessing the counters at one time, but the values of those counters are still subject to change even while the mutex is in effect.

So, you're marking the vars volatile because they can be externally modified, and not because they're inside a mutex guard.

Keep them volatile.

谈情不如逗狗 2024-08-16 07:42:29

虽然这可能取决于您使用的线程库,但我的理解是任何像样的库都不需要需要使用易失性

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

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

While this may depend on the threading library you are using, my understanding is that any decent library will not require use of volatile.

In Pthreads, for example, use of a mutex will ensure that your data gets committed to memory correctly.

EDIT: I hereby endorse tony's answer as being better than my own.

抚笙 2024-08-16 07:42:29

您仍然需要“易失性”关键字。

互斥体防止计数器并发访问。

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

You still need the "volatile" keyword.

The mutexes prevent the counters from concurrent access.

"volatile" tells the compiler to actually use the counter
instead of caching it into a CPU register (which would not
be updated by the concurrent thread).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文