以下内容摘自Concurrent Programming on windows,第10章Page 528~529,c ++模板双重检查实施
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将从关键部分访问),并运行here中的示例代码,我认为:
障碍不是围栏..应该注意障碍效应 缓存中的一切。围栏会影响单个缓存行。
除非绝对必要,否则不应增加障碍。使用 一个围栏,你可以选择一个_Interlocked内在函数。
正如作者写道:“ X86 Intel64和AMD64处理器都不需要围栏”,这是因为这些平台只允许存储加载重新排序。
还有一个问题,编译是否理解调用Enter / Leave关键部分的语义?如果没有,那么它可能会按照以下答案进行优化,这将导致不良行为。
由于
答案 0 :(得分:2)
Joe Duffy认为_ReadBarrier和_WriteBarrier编译器内在函数都是编译器和处理器级别的围栏。在Concurrent Programming on windows,第515页,他写了
一组编译器内在函数强制编译器和处理器级别 VC ++中的栅栏:_ReadWriteBarrier发出一个完整的栅栏,_ReadBarrier 发出只读栅栏,_WriteBarrier发出只写栅栏。
auther依赖于_ReadBarrier和_WriteBarrier编译器内在函数来防止编译器和硬件重新排序。
_ReadWriteBarrier编译器内在函数的MSDN文档不支持编译器内在函数影响硬件级别的假设。 Visual Studio 2010和Visual Studio 2008的MSDN文档明确否认编译器内在函数适用于硬件级别:
_ReadBarrier,_WriteBarrier和_ReadWriteBarrier编译器内在函数仅阻止编译器重新排序。要防止CPU重新排序读写操作,请使用MemoryBarrier宏。
Visual Studio 2005和Visual Studio .NET 2003的MSDN文档没有这样的说明。它没有说明内在函数是否适用于硬件级别。
如果_ReadBarrier和_WriteBarrier确实没有强制执行硬件围栏,则代码不正确。
Joe Duffy在他的书中使用术语 fence 来表示硬件和内存防护。在第511页,他写道:
围栏也常被称为障碍。英特尔似乎更喜欢“围栏”术语,而AMD更喜欢“障碍”。我也更喜欢“围栏”,这就是我在本书中使用的内容。
我相信硬件围栏也有隐式编译障碍(禁用编译优化)
Synchronization and Multiprocessor Issues文章确认硬件障碍也会影响编译器:
这些说明(内存屏障)还可确保编译器禁用任何可能跨屏障重新排序内存操作的优化。
但是,MemoryBarrier macro的MSDN文档表明并不总是阻止编译器重新排序:
创建一个硬件内存屏障(fence),防止CPU重新排序读写操作。它还可能阻止编译器重新排序读写操作。
实际上,如果编译器可以重新排序它周围的内存操作,我不明白如何使用硬件围栏。我们不确定围栏是否占据了正确的位置。
答案 1 :(得分:1)
<强> 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.
对于编译器,new
表达式将是可以在另一个之前移动的几个步骤。它的语义是分配,初始化,然后将地址分配给pValue
:
int* pTmp = new int;
*pTmp = 42;
int* pValue = *pTmp;
在顺序程序中,如果某些命令在其他命令之后移动,则语义不会改变。特别是可以在内存分配和第一次访问之间自由移动赋值,即第一次取消引用其中一个指针,包括在新表达式之后赋值指针值之后:
int* pTmp = new int;
int* pValue = *pTmp;
m_pValue = pValue;
*pTmp = 42;
//m_pValue now points to a newly allocated int with value 42.
编译器可能会这样做来优化大多数临时指针:
m_pValue = new int;
*m_pValue = 42;
//m_pValue now points to a newly allocated int with value 42.
这是顺序程序的正确语义。
我相信LeaveCriticalSection有隐式硬件围栏。因此,在分配给m_pValue之前的任何写作都将被同步。
没有。栅栏是在赋值给m_pValue之后,但编译器仍然可以在它和栅栏之间移动整数赋值:
m_pValue = new int;
*m_pValue = 42;
LeaveCriticalSection();
而且为时已晚,因为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(); |