12 c# c++ parallel-processing multithreading lock-free
我有一个关于以下代码示例的问题(m_value不是volatile,每个线程都在一个单独的处理器上运行)
void Foo() // executed by thread #1, BEFORE Bar() is executed
{
Interlocked.Exchange(ref m_value, 1);
}
bool Bar() // executed by thread #2, AFTER Foo() is executed
{
return m_value == 1;
}
Run Code Online (Sandbox Code Playgroud)
在Foo()中使用Interlocked.Exchange是否保证在执行Bar()时,我会看到值"1"?(即使值已存在于寄存器或缓存行中?)或者在读取m_value的值之前是否需要设置内存屏障?
另外(与原始问题无关),声明一个volatile成员并通过引用InterlockedXX方法传递它是否合法?(编译器警告通过引用传递volatile,所以在这种情况下我应该忽略警告吗?)
请注意,我不是在寻找"更好的做事方式",所以请不要发布建议完全替代方式("使用锁定"等)的答案,这个问题来自于纯粹的兴趣..
记忆障碍对您没有特别的帮助.它们指定了内存操作之间的顺序,在这种情况下,每个线程只有一个内存操作,因此无关紧要.一个典型的场景是非原子地写入结构中的字段,内存屏障,然后将结构的地址发布到其他线程.Barrier保证在获取结构成员的地址之前,所有CPU都可以看到对结构成员的写入.
你真正需要的是原子操作,即.InterlockedXXX函数或C#中的volatile变量.如果Bar中的读取是原子的,那么你可以保证编译器和cpu都不会做任何阻止它在Foo写入之前读取值的优化,或者在Foo中写入后根据首先执行的执行.因为你说你"知道"Foo的写作发生在Bar的阅读之前,所以Bar总是会返回true.
如果没有读取Bar是原子的,它可能是读取部分更新的值(即垃圾),或缓存的值(来自编译器或来自CPU),这两者都可能阻止Bar返回true应该是什么.
大多数现代CPU保证字对齐读取是原子的,所以真正的诀窍是你必须告诉编译器读取是原子的.
内存屏障使用的常见模式与您在关键部分的实现中放置的模式相匹配,但分为生产者和消费者对。例如,您的关键部分实现通常采用以下形式:
while (!pShared->lock.testAndSet_Acquire()) ; //(这个循环应该包括所有正常的临界区内容,例如 // 旋转,浪费, //暂停()指令,以及资源上的最后手段放弃和阻塞 // 直到锁可用。) // 访问共享内存。 pShared->foo = 1 v = pShared-> 咕咕 pShared->lock.clear_Release()
上面的获取内存屏障可确保在成功锁定修改之前可能已启动的任何加载(pShared->goo)被丢弃,并在必要时重新启动。
释放内存屏障确保在保护共享内存的锁字被清除之前,从 goo 到(本地)变量 v 的加载已完成。
在典型的生产者和消费者原子标志场景中,您有类似的模式(很难通过您的示例判断这是否是您正在做的事情,但应该说明这个想法)。
假设您的生产者使用原子变量来指示某些其他状态已准备好使用。你会想要这样的东西:
pShared->goo = 14 pShared->atomic.setBit_Release()
如果生产者中没有“写入”屏障,您无法保证硬件在 goo 存储通过 cpu 存储队列并通过其可见的内存层次结构之前不会到达原子存储(即使您有一种机制可以确保编译器按照您想要的方式排序)。
在消费者中
if ( pShared->atomic.compareAndSwap_Acquire(1,1) )
{
v = pShared->goo
}
如果没有“读取”屏障,您将不知道在原子访问完成之前硬件还没有为您获取数据。原子(即:使用互锁函数操作的内存,执行诸如锁定 cmpxchg 之类的操作)仅就其本身而言是“原子”的,而不是其他内存。
现在,剩下要提到的是屏障结构非常不可移植。您的编译器可能为大多数原子操作方法提供 _acquire 和 _release 变体,这些是您使用它们的方式。根据您使用的平台(即:ia32),这些很可能正是您在没有 _acquire() 或 _release() 后缀的情况下得到的。重要的平台是 ia64(实际上已经死了,除了 HP 上它仍然轻微抽搐)和 powerpc。ia64 对大多数加载和存储指令(包括像 cmpxchg 这样的原子指令)都有 .acq 和 .rel 指令修饰符。powerpc对此有单独的说明(isync和lwsync分别为您提供读和写屏障)。
现在。说了这么多。你真的有充分的理由走这条路吗?正确地完成这一切可能非常困难。为代码审查中的大量自我怀疑和不安全感做好准备,并确保您对各种随机时序场景进行大量高并发测试。除非您有非常充分的理由避免使用关键部分,否则请使用关键部分,并且不要自己编写该关键部分。