即使对于一个简单的2线程通信示例,我也很难用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);
}
据我所知,以上代码将正确地命令存储在桶中的->标志-存储->标志-加载从桶加载->。但是,我认为在从桶加载到用新数据重写桶之间仍然存在竞争条件。要强制执行桶读之后的命令,我想我需要在桶读和下面的atomic_store之间设置一个显式的atomic_store。不幸的是,似乎没有memory_order
参数在前面的负载上强制执行任何东西,甚至memory_order_seq_cst
也没有。
一个非常脏的解决方案可能是在使用者线程中重新分配带有虚拟值的bucket
:这与使用者只读概念相矛盾。
在较老的C99/GCC世界中,我可以使用传统的__sync_synchronize()
,我相信它会足够强大。
什么是更好的C11风格的解决方案来同步这个所谓的反依赖?
(当然,我知道我应该避免这样的低级别编码,并使用可用的高级构造,但我想了解.)
发布于 2013-10-31 13:45:09
要强制执行桶读之后的命令,我想我需要在桶读和下面的atomic_store之间使用一个显式的atomic_store()。
我不认为atomic_thread_fence()
调用是必要的:标志更新具有发布语义,防止了前面的任何加载或存储操作被重新排序。见Herb Sutter的正式定义:
一个写发行版执行,毕竟它是按照程序顺序读取和写入的线程。
这将防止在bucket
更新之后重新排序flag
的读取,而不管编译器选择在何处存储data
。
这使我想到你对另一个答案的评论:
volatile
确保生成ld/st操作,这些操作随后可以用栅栏排序。但是,数据是局部变量,而不是易失性的。编译器可能会将其放入寄存器中,从而避免存储操作。这就使得桶中的负载将与随后的标志重置一起排序。
如果bucket
读取不能通过flag
的写入版本重新排序,那么这似乎不是一个问题,因此不应该需要volatile
(尽管拥有它也可能没有什么坏处)。这也是不必要的,因为大多数函数调用(在本例中是atomic_store_explicit(&flag)
)充当编译时内存屏障。编译器不会通过非内联函数调用重新排序全局变量的读取,因为该函数可以修改相同的变量。
我也同意@MaximYegorushkin的观点,即当目标是兼容的架构时,您可以改进繁忙的等待pause
指令。GCC和国际商会似乎都有_mm_pause(void)
的本质(可能相当于__asm__ ("pause;")
)。
发布于 2013-10-30 20:30:09
我同意MikeStrobel在他的评论中所说的话。
这里不需要atomic_thread_fence()
,因为关键部分从获取开始,最后以发布语义结束。因此,在获取和写入发布后之前,不能对关键部分中的读取进行重新排序。这就是为什么volatile
在这里也是不必要的。
此外,我看不出为什么这里不使用(线程)自旋锁。spinlock为您做了类似的繁忙旋转,但它也使用 instruction。
暂停内禀用于自旋等待循环,处理器实现动态执行(特别是无序执行)。在旋转等待循环中,暂停内在提高了代码检测锁释放的速度,并提供了特别重要的性能增益。下一条指令的执行会被延迟一段特定于实现的时间.暂停指令不修改体系结构状态。对于动态调度,暂停指令减少了退出自旋循环的代价.
发布于 2013-10-30 21:24:09
直接答覆:
存储是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_...
抽象下面的汇编程序指令,所以保证它是最快的方法。
https://stackoverflow.com/questions/19689872
复制相似问题