以下摘自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;
}
正如作者所说:
_WriteBarrier 是在实例化对象之后但在 m_pValue 字段中写入指向它的指针之前找到的。这是确保对象初始化中的写入永远不会延迟超过对 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 将在临界区之外访问),并从这里运行示例代码,我认为:
Barrier 不是栅栏。应该注意的是,Barrier 会影响缓存中的所有内容。栅栏影响单个高速缓存行。
除非绝对必要,否则不应添加障碍。要使用栅栏,您可以选择 _Interlocked 内部函数之一。
正如作者所写:“ X86 Intel64 和 AMD64 处理器都不需要栅栏”,这是因为这些平台只允许存储加载重新排序。
还有一个问题,编译器是否理解调用 Enter/Leave 临界区的语义?如果没有,那么它可能会按照以下答案进行优化,这将导致不良行为。
谢谢