[我和同事一起为在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 ++ 0x,不是。而且它没有在C中指定。因此,它实际上取决于编译器。通常,如果编译器不能保证它会遵守涉及多个线程的函数或操作对内存访问的排序约束,则您将无法使用该编译器编写多线程安全代码。参见汉斯·J·博姆的Threads Cannot be Implemented as a Library。如果上面的代码是正确的,那么它对于缓存是无害的问题?
关于编译器应支持线程安全代码的抽象,Memory Barriers上的Wikipedia条目是一个很好的起点。
((为什么人们建议使用volatile
,一些编译器将volatile
视为编译器的内存屏障。这绝对不是标准的。)
volatile
。长答案是,像关键部分一样的顺序点也取决于平台,就像您使用的任何线程解决方案一样,因此大多数线程安全性也取决于平台。
C ++ 0x具有线程和线程安全性的概念,但是当前标准没有,因此有时将volatile
误识别为某种东西,以防止多线程编程中操作和内存访问的重新排序,而这从没想过并且可以做。不能以这种方式可靠地使用。
volatile
在C ++中唯一应使用的是允许访问内存映射的设备,允许使用setjmp
和longjmp
之间的变量,以及允许使用信号处理程序中的sig_atomic_t
变量。关键字本身不会使变量成为原子。
C ++ 0x中的好消息,我们将拥有STL构造std::atomic
,该构造可用于保证原子操作和变量的线程安全构造。在您选择的编译器支持它之前,您可能需要转到boost库或清除一些汇编代码来创建自己的对象以提供原子变量。
P.S。许多混淆是由Java和.NET实际上使用关键字volatile
强制执行多线程语义引起的,但是C ++却适用于C,在这种情况下并非如此。
memory
破坏符充当编译器障碍。实际上,有两件事可以保护您的代码免于(编译器)缓存:
pthread_mutex_*()
),这意味着编译器不知道该函数不会修改您的全局变量,因此必须重新加载它们。pthread_mutex_*()
包含编译器障碍,例如:在glibc / x86上,pthread_mutex_lock()
最终调用了具有lll_lock()
破坏符的宏lll_lock()
,迫使编译器重新加载变量。由互斥体保护的变量在不同线程进行读写时,保证是正确的。需要使用线程API来确保此类变量视图一致。此访问权限是程序逻辑的全部部分,而volatile关键字与此处无关。
互斥量代码非常复杂:一个好的优化互斥量锁定/解锁代码包含了即使是优秀的程序员也难以理解的那种代码。它使用特殊的比较和设置指令,不仅管理解锁/锁定状态,还管理等待队列,还可以选择使用系统调用进入等待状态(用于锁定)或唤醒其他线程(用于解锁)。
[无论如何,普通编译器都无法解码和“理解”所有复杂代码(同样,除了简单的自旋锁之外),因此即使对于编译器也不知道互斥量是什么,以及如何它与同步有关,实际上,编译器无法围绕此类代码进行任何优化
。这是代码是“内联”的,还是可以用于跨模块优化目的的分析,或者是否可以使用全局优化。我认为编译器实际上并不了解pthread_mutex_lock()是一个特殊的函数,因此我们只是受到保护按顺序点?
编译器不知道其作用,因此不会尝试对其进行优化。
如何“特殊”?它是不透明的,因此被视为。
在不透明函数中并不特殊
。可以访问任何其他对象的任意不透明函数在语义上没有区别。我担心的是缓存。编译器能否放置_protected的副本在堆栈或寄存器中,并在作业?
是的,通过使用变量名或指针,透明且直接地作用于对象的代码
以编译器可以遵循的方式。不在可能使用任意指针间接使用变量的代码中。
所以是在调用不透明函数之间
。不对。还有对于只能在函数中使用的变量
,按名称:对于没有地址或未绑定引用的局部变量(这样编译器将无法遵循所有其他用法)。实际上,可以在包括锁定/解锁在内的任意调用之间“缓存”它们。如果没有,是什么阻止了这种情况的发生?是这个的变体模式易受攻击?
功能的不透明度。非内联。汇编代码。系统调用。代码复杂度。使编译器摆脱困境的所有事物都认为“那是复杂的东西,只需对其进行调用”。
编译器的默认位置始终是“愚蠢地执行,无论如何我还是不明白正在做什么”]不是“我会优化/让我们重写我更了解的算法”。大多数代码并未以复杂的非本地方式进行优化。
现在让我们假设绝对更糟
(从编译器应该放弃的角度来看,从优化算法的角度来看这绝对是最好的):我们可能会遇到问题,因为编译器可以围绕函数调用进行优化
或受保护的变量是否需要是可变的。您可以使它易失是出于使事物变得易变的通常原因:确定能够访问调试器中的变量,防止浮点变量在运行时具有错误的数据类型,等等。>
使其具有可变性实际上甚至无法解决上述问题,因为
可变性本质上是具有I / O操作语义的抽象机中的存储操作,并且>因此仅针对]进行排序]
像iostream这样的真实I / O
关于非易失性存储器副作用,Volatile不排序。