互斥锁功能是否足以实现无挥发性?

问题描述 投票:39回答:5

[我和同事一起为在x86,x64,Itanium,PowerPC和其他使用了10年的服务器CPU上运行的各种平台编写软件。

我们只是讨论了诸如pthread_mutex_lock()... pthread_mutex_unlock()之类的互斥函数本身是否足够,或者受保护的变量是否需要是可变的。

int foo::bar()
{
 //...
 //code which may or may not access _protected.
 pthread_mutex_lock(m);
 int ret = _protected;
 pthread_mutex_unlock(m);
 return ret;
}

我担心的是缓存。编译器是否可以将_protected的副本放置在堆栈或寄存器中,并在分配中使用该过时的值?如果没有,是什么阻止了这种情况的发生?这种模式的变化是否容易受到影响?

我认为编译器实际上并不了解pthread_mutex_lock()是一个特殊函数,所以我们是否仅受序列点保护?

非常感谢。

更新:好的,我可以看到一个趋势,并给出了解释为什么挥发物不好的答案。我尊重这些答案,但是可以轻松地在网上找到有关该主题的文章。我在网上找不到的原因以及我问这个问题的原因是如何保护[[without volatile。 如果上面的代码是正确的,如何对缓存问题无害?

c++ multithreading mutex volatile memory-barriers
5个回答
8
投票

如果上面的代码是正确的,那么它对于缓存是无害的问题?

直到C ++ 0x,不是。而且它没有在C中指定。因此,它实际上取决于编译器。通常,如果编译器不能保证它会遵守涉及多个线程的函数或操作对内存访问的排序约束,则您将无法使用该编译器编写多线程安全代码。参见汉斯·J·博姆的Threads Cannot be Implemented as a Library

关于编译器应支持线程安全代码的抽象,Memory Barriers上的Wikipedia条目是一个很好的起点。

((为什么人们建议使用volatile,一些编译器将volatile视为编译器的内存屏障。这绝对不是标准的。)


14
投票
最简单的答案是多线程根本不需要volatile

长答案是,像关键部分一样的顺序点也取决于平台,就像您使用的任何线程解决方案一样,因此大多数线程安全性也取决于平台。

C ++ 0x具有线程和线程安全性的概念,但是当前标准没有,因此有时将volatile误识别为某种东西,以防止多线程编程中操作和内存访问的重新排序,而这从没想过并且可以做。不能以这种方式可靠地使用。

volatile在C ++中唯一应使用的是允许访问内存映射的设备,允许使用setjmplongjmp之间的变量,以及允许使用信号处理程序中的sig_atomic_t变量。关键字本身不会使变量成为原子。

C ++ 0x中的好消息,我们将拥有STL构造std::atomic,该构造可用于保证原子操作和变量的线程安全构造。在您选择的编译器支持它之前,您可能需要转到boost库或清除一些汇编代码来创建自己的对象以提供原子变量。

P.S。许多混淆是由Java和.NET实际上使用关键字volatile强制执行多线程语义引起的,但是C ++却适用于C,在这种情况下并非如此。


8
投票
您的线程库应在互斥锁锁定和解锁时包含适当的CPU和编译器屏障。对于GCC,asm语句上的memory破坏符充当编译器障碍。

实际上,有两件事可以保护您的代码免于(编译器)缓存:

    您正在调用一个非纯外部函数(pthread_mutex_*()),这意味着编译器不知道该函数不会修改您的全局变量,因此必须重新加载它们。
  • 正如我说的,pthread_mutex_*()包含编译器障碍,例如:在glibc / x86上,pthread_mutex_lock()最终调用了具有lll_lock()破坏符的宏lll_lock(),迫使编译器重新加载变量。

2
投票
volatile关键字向编译器提示,该变量可能会在程序逻辑之外更改,例如内存映射的硬件寄存器可能会在中断服务例程中更改。这样可以防止编译器假设缓存的值始终正确,并且通常会强制读取内存以检索该值。这种用法使线程化大约提前了几十年。我已经看到它也可以通过信号操作变量,但是我不确定用法是否正确。

由互斥体保护的变量在不同线程进行读写时,保证是正确的。需要使用线程API来确保此类变量视图一致。此访问权限是程序逻辑的全部部分,而volatile关键字与此处无关。


0
投票
除了最简单的自旋锁算法之外,

互斥量代码非常复杂:一个好的优化互斥量锁定/解锁代码包含了即使是优秀的程序员也难以理解的那种代码。它使用特殊的比较和设置指令,不仅管理解锁/锁定状态,还管理等待队列,还可以选择使用系统调用进入等待状态(用于锁定)或唤醒其他线程(用于解锁)。

[无论如何,普通编译器都无法解码和“理解”所有复杂代码(同样,除了简单的自旋锁之外),因此即使对于编译器也不知道互斥量是什么,以及如何它与同步有关,

实际上,编译器无法围绕此类代码进行任何优化

这是代码是“内联”的,还是可以用于跨模块优化目的的分析,或者是否可以使用全局优化。

我认为编译器实际上并不了解pthread_mutex_lock()是一个特殊的函数,因此我们只是受到保护按顺序点?

编译器不知道其作用,因此不会尝试对其进行优化。

如何“特殊”?它是不透明的,因此被视为。

在不透明函数中并不特殊

可以访问任何其他对象的任意不透明函数在语义上没有区别。

我担心的是缓存。编译器能否放置_protected的副本在堆栈或寄存器中,并在作业?

是的,通过使用变量名或指针,透明且直接地作用于对象的代码

以编译器可以遵循的方式。不在可能使用任意指针间接使用变量的代码中。

所以是

在调用不透明函数之间

。不对。还有

对于只能在函数中使用的变量

,按名称:对于没有地址或未绑定引用的局部变量(这样编译器将无法遵循所有其他用法)。实际上,可以在包括锁定/解锁在内的任意调用之间“缓存”它们。
如果没有,是什么阻止了这种情况的发生?是这个的变体模式易受攻击?

功能的不透明度。非内联。汇编代码。系统调用。代码复杂度。使编译器摆脱困境的所有事物都认为“那是复杂的东西,只需对其进行调用”。

编译器的默认位置始终是“愚蠢地执行,无论如何我还是不明白正在做什么”]不是“我会优化/让我们重写我更了解的算法”。大多数代码并未以复杂的非本地方式进行优化。

现在

让我们假设绝对更糟

(从编译器应该放弃的角度来看,从优化算法的角度来看这绝对是最好的):
    该函数是“内联”(=可用于内联)(或启动了全局优化,或者所有函数在道德上都是“内联”;]]在该同步原语(锁定或解锁)中,需要
  • 无内存障碍
  • (例如,在单处理器分时系统中,在多处理器强序系统中),因此它不包含此类内容;
  • 没有特殊说明
  • (例如比较和设置)(例如,对于自旋锁,解锁操作是简单的写操作);
  • 没有系统调用
  • 来暂停或唤醒线程(自旋锁中不需要);
    然后

    我们可能会遇到问题,因为编译器可以围绕函数调用进行优化

这通过插入编译器障碍(例如,空的asm语句和其他可访问变量的“ clobber”来解决,这很简单。这意味着编译器仅假设被调用函数可访问的所有内容均为“ clobbered”。
或受保护的变量是否需要是可变的。

您可以使它易失是出于使事物变得易变的通常原因:确定能够访问调试器中的变量,防止浮点变量在运行时具有错误的数据类型,等等。>

使其具有可变性实际上甚至无法解决上述问题,因为

可变性本质上是具有I / O操作语义的抽象机中的存储操作,并且>因此仅针对]进行排序]

    像iostream这样的真实I / O

  • 系统调用
  • 其他易失性操作
  • asm内存破坏者(但在这些方面没有重新排列内存副作用)
  • 对外部函数的调用(它们可能会执行上述操作之一)
  • 关于非易失性存储器副作用,Volatile不排序。

  • 即使在最特殊的情况下volatile将先验帮助,在不需要内存防护的情况下:在单个CPU上的分时系统上编写线程原语时。 (这可能是C或C ++鲜为人知的方面之一。)因此,虽然volatile确实可以防止“缓存”,但volatile甚至都不能阻止编译器对锁定/解锁操作进行重新排序,除非所有共享变量都为volatile。
    © www.soinside.com 2019 - 2024. All rights reserved.