Cha*_*ang 6 c++ concurrency multithreading atomic memory-barriers
以下摘自Concurrent Programming on windows,第10章第528~529页,一个c++模板Double check实现
T getValue(){
if (!m_pValue){
EnterCriticalSection(&m_crst);
if (! m_pValue){
T pValue = m_pFactory();
_WriteBarrier();
m_pValue = pValue;
}
LeaveCriticalSection(&m_crst);
}
_ReadBarrier();
return m_pValue;
}
Run Code Online (Sandbox Code Playgroud)
正如作者所说:
在实例化对象之后,但在 m_pValue 字段中写入指向它的指针之前,会找到 _WriteBarrier。这是确保对象初始化中的写入永远不会延迟到对 m_pValue 本身的写入之后所必需的。
由于_WriteBarrier 是编译屏障,如果编译知道LeaveCriticalSection 的语义,我认为它没有用。编译可能会省略对 pValue 的写入,但永远不会优化在函数调用之前移动赋值,否则会违反程序语义。我相信 LeaveCriticalSection 具有隐式硬件围栏。因此,在分配给 m_pValue 之前的任何写入都将被同步。
另一方面,如果编译不知道 LeaveCriticalSection 的语义,则所有平台都需要 _WriteBarrier以防止编译将赋值移出临界区。
而对于_ReadBarrier,作者说
类似地,我们在返回 m_value 之前需要一个 _ReadBarrier,以便调用 getValue 之后的加载不会重新排序在调用之前发生。
首先,如果这个函数包含在一个库中,并且没有可用的源代码,那么编译如何知道是否存在编译障碍?
其次,如果需要,它会被放置在错误的位置,我认为我们需要将它放在 EnterCriticalSection 之后以表达获取栅栏。与我上面写的类似,这取决于编译是否理解 EnterCriticalSection 的语义。
而且作者还说:
但是,我还要指出,X86、Intel64 和 AMD64 处理器都不需要栅栏。不幸的是,像 IA64 这样的弱处理器已经把水搅浑了
正如我上面分析的,如果我们在某个平台上需要那些屏障,那么我们在所有平台上都需要它们,因为那些屏障是编译屏障,它只是确保编译可以做正确的优化,以防万一他们不明白一些函数的语义。
如果我错了,请纠正我。
另一个问题,msvc 和 gcc 有什么参考可以指出他们理解同步语义的函数吗?
更新 1:根据答案(m_pValue 将在临界区外访问),并从此处运行示例代码,我认为:
屏障不是栅栏。应该注意的是,屏障会影响缓存中的所有内容。栅栏影响单个缓存行。
除非绝对必要,否则不应添加障碍。要使用围栏,您可以选择 _Interlocked 内部函数之一。
正如作者所写:“ X86 Intel64 和 AMD64 处理器都不需要栅栏”,这是因为这些平台只允许存储加载重新排序。
还有一个问题,编译是否理解调用 Enter/Leave 临界区的语义?如果没有,那么它可能会按照以下答案进行优化,这将导致不良行为。
谢谢
tl;dr:
工厂调用很可能采取几个步骤,这些步骤可以在分配给 后移动m_pValue。该表达式!m_pValue将在工厂调用完成之前返回 false,从而在第二个线程中给出不完整的返回值。
解释:
编译可能会省略对 pValue 的写入,但切勿优化以在函数调用之前移动赋值,否则会违反程序语义。
不必要。将 T 视为int*,工厂方法创建一个新的 int 并将其初始化为 42。
int* pValue = new int(42);
m_pValue = pValue;
//m_pValue now points to anewly allocated int with value 42.
Run Code Online (Sandbox Code Playgroud)
对于编译器来说,new表达式将是可以移动到另一个步骤之前的几个步骤。它的语义是分配、初始化,然后将地址分配给pValue:
int* pTmp = new int;
*pTmp = 42;
int* pValue = *pTmp;
Run Code Online (Sandbox Code Playgroud)
在顺序程序中,如果某些命令移到其他命令之后,语义不会改变。特别是,赋值可以在内存分配和第一次访问之间自由移动,即第一次取消引用其中一个指针,包括在 new 表达式之后分配指针值之后:
int* pTmp = new int;
int* pValue = *pTmp;
m_pValue = pValue;
*pTmp = 42;
//m_pValue now points to a newly allocated int with value 42.
Run Code Online (Sandbox Code Playgroud)
编译器可能会这样做来优化大部分临时指针:
m_pValue = new int;
*m_pValue = 42;
//m_pValue now points to a newly allocated int with value 42.
Run Code Online (Sandbox Code Playgroud)
这是顺序程序的正确语义。
我相信 LeaveCriticalSection 有隐式硬件围栏。因此,在分配给 m_pValue 之前的任何写入都将被同步。
不会。栅栏位于 m_pValue 赋值之后,但编译器仍然可以在栅栏和 m_pValue 之间移动整数赋值:
m_pValue = new int;
*m_pValue = 42;
LeaveCriticalSection();
Run Code Online (Sandbox Code Playgroud)
但这已经太晚了,因为 Thread2 不需要进入 CriticalSection:
Thread 1: | Thread 2:
|
m_pValue = new int; |
| if (!m_pValue){ //already false
| }
| return m_pValue;
| /*use *m_pValue */
*m_pValue = 42; |
LeaveCriticalSection(); |
Run Code Online (Sandbox Code Playgroud)