通常内存栅使用的模式与临界区实现相同,但被拆分成生产者和消费者的一对。例如,您的临界区实现通常是以下形式:
while (!pShared->lock.testAndSet_Acquire()) ;
// (此循环应包括所有正常的临界区内容,如
// 自旋、浪费、
// pause() 指令以及最后的放弃并阻塞资源
// 直到锁定可用。)
// 访问共享内存。
pShared->foo = 1
v = pShared->goo
pShared->lock.clear_Release()
上面的 Acquire 内存栅确保在成功修改锁之前可能已经开始的任何加载(pShared->goo)都会被取消,以便在必要时重新启动。
Release 内存栅确保 goo 到变量 v 的加载在保护共享内存的锁清除之前完成。
在典型的生产者和消费者原子标志场景中,您有一个类似的模式(通过示例很难确定是否是这样,但应该可以说明这个想法)。
假设您的生产者使用原子变量指示某些其他状态已准备好使用。你需要像这样的东西:
pShared->goo = 14
pShared->atomic.setBit_Release()
如果生产者没有“写”栅,那么您无法保证硬件不会在 goo 存储通过 CPU 存储队列并上升到可见的内存层次结构之前到达原子存储。即使您有一个确保编译器按照您想要的方式排序的机制。
在消费者中:
if ( pShared->atomic.compareAndSwap_Acquire(1,1) )
{
v = pShared->goo
}
如果没有“读”栅,您将不知道硬件是否在完成原子访问之前已经获取了 goo。原子(即:使用 Interlocked 函数操作的内存,如 lock cmpxchg),仅针对其本身而不是其他内存是“原子”的。
现在,必须提到的剩余事项是,栅构造高度不可移植。您的编译器可能为大多数原子操作方法提供 _acquire 和 _release 变体,并且这些是您将使用它们的方式。根据您使用的平台(即:ia32),这些很可能就是没有 _acquire() 或 _release() 后缀时得到的内容。在 ia64 平台上(实际上除了 HP 以外已经死亡,在 HP 上仍然略有动静),大多数加载和存储指令(包括原子指令,如 cmpxchg)都有 .acq 和 .rel 指令修饰符。PowerPC 有专门的指令(isync 和 lwsync 分别提供读和写栅)。
现在,说了这么多,你真的有充足的理由选择这条路吗?正确地完成这一切可能非常困难。准备好在代码审查中面对大量的自我怀疑和不安,并确保进行大量的高并发测试,包括各种随机时间场景。除非你有非常非常充分的理由避免使用关键部分,否则请使用关键部分,并且不要自己编写该关键部分。