即使是一个简单的双线程通信示例,我也很难用C11原子和memory_fence样式来表达它,以获得正确的内存排序:
共享数据:
volatile int flag, bucket;
生产者线程:
while (true) {
int value = producer_work();
while (atomic_load_explicit(&flag, memory_order_acquire))
; // busy wait
bucket = value;
atomic_store_explicit(&flag, 1, memory_order_release);
}
消费者话题:
while (true) {
while (!atomic_load_explicit(&flag, memory_order_acquire))
; // busy wait
int data = bucket;
atomic_thread_fence(/* memory_order ??? */);
atomic_store_explicit(&flag, 0, memory_order_release);
consumer_work(data);
}
据我了解,上面的代码可以正确地订购存储桶 - >旗舰店 - > flag-load - >负载从桶。但是,我认为在load-from-bucket和使用新数据重新写入桶之间仍存在竞争条件。要在读取桶之后强制执行订单,我想在存储桶读取和以下atomic_store之间需要显式atomic_thread_fence()
。不幸的是,似乎没有memory_order
参数来强制执行前面加载的任何操作,甚至不是memory_order_seq_cst
。
一个非常脏的解决方案可能是在消费者线程中使用虚拟值重新分配bucket
:这与消费者只读概念相矛盾。
在较旧的C99 / GCC世界中,我可以使用我认为足够强大的传统__sync_synchronize()
。
同步这种所谓的反依赖性的更好的C11风格解决方案是什么?
(当然我知道我应该更好地避免这种低级编码并使用可用的更高级别的结构,但我想理解......)
答案 0 :(得分:3)
要在读取桶之后强制执行订单,我想我需要在读取桶和以下atomic_store之间使用显式的atomic_thread_fence()。
我不相信atomic_thread_fence()
调用是必要的:标志更新具有释放语义,防止任何先前的加载或存储操作被重新排序。参见Herb Sutter的正式定义:
写入版本在所有读取和写入之后执行 由程序顺序之前的同一个线程执行。
这可以防止在bucket
更新后重新排序flag
的读取,无论编译器选择存储data
的位置。
这让我对你的另一个答案发表评论:
volatile
确保生成ld / st操作,随后可以使用fences进行排序。但是,数据是局部变量,不是易失性的。编译器可能会将其置于寄存器中,从而避免存储操作。这使得来自桶的负载随后随着标志的重置而被订购。
如果bucket
读取无法在flag
写入版本之后重新排序,那么这似乎不是问题,因此volatile
不应该是必需的(尽管它可能不是也有伤害。这也是不必要的,因为大多数函数调用(在这种情况下,atomic_store_explicit(&flag)
)充当编译时内存屏障。编译器不会通过非内联函数调用重新排序读取全局变量,因为该函数可以修改相同的变量。
我同意@MaximYegorushkin,在针对兼容架构时,您可以使用pause
指令改善您的忙碌等待。 GCC和ICC似乎都有_mm_pause(void)
内在函数(可能相当于__asm__ ("pause;")
)。
答案 1 :(得分:1)
我同意@MikeStrobel在评论中所说的话。
这里不需要atomic_thread_fence()
,因为您的关键部分以获取开头并以释放语义结束。因此,在获取和写入发布之后,不能在关键部分中进行重新排序。这就是为什么volatile
在这里也是不必要的原因。
此外,我没有看到为什么(pthread)spinlock不在这里使用的原因。 spinlock为你做了类似的忙碌旋转,但它也使用了pause
instruction:
暂停内在函数用于自旋等待循环,处理器实现动态执行(特别是乱序执行)。在自旋等待循环中,暂停内在提高了代码检测到锁定释放的速度,并提供了特别显着的性能提升。 下一条指令的执行会延迟一段实现特定的时间。 PAUSE指令不会修改体系结构状态。对于动态调度,PAUSE指令减少了退出自旋循环的代价。
答案 2 :(得分:-1)
直接回答:
您的商店是memory_order_release操作意味着您的编译器必须在存储标志之前为存储指令发出内存栅栏。这是确保其他处理器在开始解释之前查看已发布数据的最终状态所必需的。所以,不,你不需要添加第二个围栏。
答案很长:
如上所述,编译器会将您的atomic_...
指令转换为围栏和内存访问的组合;基本抽象不是原子载荷,它是记忆围栏。这就是事情的运作方式,即使新的C ++抽象诱使你以不同的方式思考。而且我个人觉得内存围栏比C ++中受到尊重的抽象更容易思考。
从硬件角度来看,您需要确保的是您的加载和存储的相对订单,即。即在生成器中写入标志之前写入存储桶已完成,并且标志的负载读取的值大于消费者中存储桶的负载。
那就是说,你真正需要的是:
//producer
while(true) {
int value = producer_work();
while (flag) ; // busy wait
atomic_thread_fence(memory_order_acquire); //ensure that value is not assigned to bucket before the flag is lowered
bucket = value;
atomic_thread_fence(memory_order_release); //ensure bucket is written before flag is
flag = true;
}
//consumer
while(true) {
while(!flag) ; // busy wait
atomic_thread_fence(memory_order_acquire); //ensure the value read from bucket is not older than the last value read from flag
int data = bucket;
atomic_thread_fence(memory_order_release); //ensure data is loaded from bucket before the flag is lowered again
flag = false;
consumer_work(data);
}
请注意,标签“生产者”和“消费者”在这里有误导性,因为我们有两个过程打乒乓球,每个过程依次成为生产者和消费者;只是一个线程产生有用的值,而另一个产生“漏洞”以将有用的值写入...
atomic_thread_fence()
就是您所需要的,因为它直接转换为atomic_...
抽象下面的汇编指令,所以它保证是最快的方法。