c - 强制执行 C 语句的顺序?

标签 c visual-c++ synchronization memory-barriers lock-free

我遇到了一个问题,MS C 编译器重新排序某些语句,这在多线程上下文中是高优化级别的关键。我想知道如何在特定位置强制排序,同时仍然使用高级优化。 (在低优化级别,此编译器不会重新排序语句)

以下代码:

 ChunkT* plog2sizeChunk=...
 SET_BUSY(plog2sizeChunk->pPoolAndBusyFlag); // set "busy" bit on this chunk of storage
 x = plog2sizeChunk->pNext;

产生这个:

 0040130F 8B 5A 08 mov ebx,dword ptr [edx+8]
 00401312 83 22 FE and dword ptr [edx],0FFFFFFFEh 

其中对 pPoolAndBusyFlag 的写入由编译器重新排序以发生在 pNext 提取之后。

SET_BUSY 本质上是

  plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;

我认为编译器正确地决定重新排序这些访问是可以的,因为它们是针对同一结构的两个独立成员,并且这种重新排序对单线程执行的结果没有影响:

typedef struct chunk_tag{
unsigned pPoolAndBusyFlag;      // Contains pointer to owning pool and a busy flag
natural log2size;                   // holds log2size of the chunk if Busy==false
struct chunk_tag* pNext;            // holds pointer to next block of same size
struct chunk_tag* pPrev;            // holds pointer to previous block of same size
} ChunkT, *pChunkT;

为了我的目的,必须先设置 pPoolAndBusyFlag,然后才能在多线程/多核上下文中对该结构的其他访问有效。我不认为这个 特定访问对我来说是有问题的,但编译器可以重新排序这一事实 意味着我的代码的其他部分可能有相同类型的重新排序,但它可能 对那些地方持批评态度。 (假设这两个语句是对两个语句的更新 成员而不是一个写/一个读)。我希望能够强制执行操作的顺序。

理想情况下,我会这样写:

 plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
 #pragma no-reordering // no such directive appears to exist
 pNext = plog2sizeChunk->pNext;

我已经通过实验验证了我可以用这种丑陋的方式获得这种效果:

 plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
 asm {  xor eax, eax }  // compiler won't optimize past asm block
 pNext = plog2sizeChunk->pNext;

给予

 0040130F 83 22 FE             and         dword ptr [edx],0FFFFFFFEh  
 00401312 33 C0                xor         eax,eax  
 00401314 8B 5A 08             mov         ebx,dword ptr [edx+8]  

我注意到 x86 硬件可能会重新排序这些特定指令,因为它们不引用相同的内存位置,并且读取可能会通过写入;要真正修复这个 示例,我需要某种类型的内存屏障。回到我之前的评论,如果它们都是写入,x86 不会对它们重新排序,写入顺序将被其他线程看到。所以在那种情况下,我认为我不需要内存屏障,只需要强制排序。

我还没有看到编译器重新排序两次写入(还),但我(还)没有仔细寻找;我刚刚被这个绊倒了。当然,仅仅因为您在此编译中看不到它而进行优化并不意味着它不会出现在下一个编译中。

那么,我如何强制编译器对它们进行排序?

我知道我可以将结构中的内存槽声明为可变的。它们仍然是独立 存储位置,所以我看不出这如何阻止优化。也许我误解了 volatile 的含义?

编辑(10 月 20 日):感谢所有响应者。我当前的实现使用 volatile(用作初始解决方案)、_ReadWriteBarrier(标记编译器不应发生重新排序的代码)和一些 MemoryBarriers(发生读写的地方),这似乎已经解决了问题.

编辑:(11 月 2 日):为了简洁起见,我最终为 ReadBarrier、WriteBarrier 和 ReadWriteBarrier 定义了一组宏。有用于前后锁定、前后解锁和一般用途的设置。其中一些是空的,一些包含 _ReadWriteBarrier 和 MemoryBarrier,适用于 x86 和基于 XCHG 的典型自旋锁 [XCHG 包含一个隐式 MemoryBarrier,因此避免了对锁前/后设置的需求)。然后我将这些放在代码中的适当位置,记录必要的(非)重新排序要求。

最佳答案

据我所知,pNext = plog2sizeChunk->pNext 发布该 block ,以便其他线程可以看到它,您必须确保它们看到正确的忙碌标志。

这意味着在发布它之前您需要一个单向内存屏障(在另一个线程中读取它之前也需要一个,尽管如果您的代码在 x86 上运行,您可以免费获得这些屏障)以确保线程实际上看到了变化。在写入之前还需要一个,以避免在它之后重新排序写入。仅仅插入程序集或使用符合标准的 volatile(MSVC volatile 提供额外的保证,尽管这在这里有所不同)是不够的 - 是的,这会阻止编译器移动读写,但 CPU不受其约束,可以在内部进行相同的重新排序。

MSVC 和 gcc 都有内部函数/宏来创建内存屏障 (see eg here)。 MSVC 还为足以解决您的问题的 volatile 提供更强的保证。最后,C++11 原子也可以工作,但我不确定 C 本身是否有任何可移植的方式来保证内存屏障。

关于c - 强制执行 C 语句的顺序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19292075/

相关文章:

c++ - 基于处理所述源图像获得的Canny边缘轮廓在opencv中对源图像进行分割

c - 从头打印链表

mfc - 如何使用 MFC 加载 .png 、 .jpeg 图像?

ios - 如何在 objective-c 中为我的一系列任务创建队列

主服务器和本地笔记本电脑之间的 MySQL 复制

iphone - 在后台加载UIImagePickerController

c - 如何通过 "win_flex bison"编写纯解析器和可重入扫描器?

c - C 中用户定义的数组大小

c++ - Gzip 压缩/解压缩长字符数组

visual-c++ - Visual C++/CLI - CLR 类 - LNK2020 错误 - 如何修复?