我有一个由一个制作人编写并由N个消费者阅读的铃声缓冲器。因为它是一个环形缓冲区,所以生产者写入的索引可以小于消费者当前的最小索引。生产者和消费者的位置由他们自己的Cursor
跟踪。
class Cursor
{
public:
inline int64_t Get() const { return iValue; }
inline void Set(int64_4 aNewValue)
{
::InterlockedExchange64(&iValue, aNewValue);
}
private:
int64_t iValue;
};
//
// Returns the ringbuffer position of the furthest-behind Consumer
//
int64_t GetMinimum(const std::vector<Cursor*>& aCursors, int64_t aMinimum = INT64_MAX)
{
for (auto c : aCursors)
{
int64_t next = c->Get();
if (next < aMinimum)
{
aMinimum = next;
}
}
return aMinimum;
}
查看生成的汇编代码,我看到:
mov rax, 922337203685477580 // rax = INT64_MAX
cmp rdx, rcx // Is the vector empty?
je SHORT $LN36@GetMinimum
npad 10
$LL21@GetMinimum:
mov r8, QWORD PTR [rdx] // r8 = c
cmp QWORD PTR [r8+56], rax // compare result of c->Get() and aMinimum
cmovl rax, QWORD PTR [r8+56] // if it's less then aMinimum = result of c->Get()
add rdx, 8 // next vector element
cmp rdx, rcx // end of the vector?
jne SHORT $LL21@GetMinimum
$LN36@GetMinimum:
fatret 0 // beautiful friend, the end
我无法看到编译器如何认为可以读取c->Get()
的值,将其与aMinimum
进行比较,然后有条件地将c->Get()
的RE-READ值移动到{{} 1}}。在我看来,这个值可能在aMinimum
和cmp
指令之间发生了变化。如果我是正确的,则可能出现以下情况:
cmovl
目前设为2
aMinimum
返回1
c->Get()
已完成且cmp
标志已设置
另一个帖子将当前less-than
当前持有的值更新为3
c
将cmovl
设为3
生产者看到3并覆盖了环形缓冲区2位的数据,即使它还没有被处理过。
我一直在看它太久了吗?不应该是这样的:
aMinimum
答案 0 :(得分:3)
你不能在访问iValue
时使用原子或任何类型的线程排序操作(可能在另一个线程上可能修改iValue
的情况也是如此,但我们& #39; ll看到那并不重要),因此编译器可以自由地假设它将在两个代码组装行之间保持不变。如果另一个线程修改了iValue
,则表示您有未定义的行为。
如果你的代码是线程安全的,那么你需要使用原子,锁或一些排序操作。
C ++ 11标准在1.10和#34;多线程执行和数据竞争&#34;中形式化了这一点,这不是特别轻松的阅读。我认为与此示例相关的部分是:
第10段:
如果
,评估A 评估B之前依赖性排序
- A对原子对象M执行释放操作,并且在另一个线程中,B对M执行消耗操作并读取由A标记的释放序列中的任何副作用写入的值,或
- 对于某些评估X,A在X之前是依赖排序的,而X对B具有依赖性。
如果我们说评估A对应Cursor::Get()
函数,评估B对应于修改iValue
的一些看不见的代码。评估A(Cursor::Get()
)不对原子对象执行任何操作,并且在其他任何事情之前没有排序依赖性(因此这里没有&#34; X&#34;这里涉及)。
如果我们说评估A对应于修改iValue
且B对应于Cursor::Get()
的代码,则可以得出相同的结论。因此,在&#34;之前没有&#34;依赖性排序; Cursor::Get()
与iValue
的修饰符之间的关系。
因此,在Cursor::Get()
修改iValue
之前,Cursor::Get()
不依赖性排序。
第11段:
评估A线程在评估B之前发生,如果
- 与B同步,或
- A是在B之前依赖订购的,或
- 进行一些评估X.
- 与X同步,X在B之前排序,或
- 在X和X之间的线程发生在B之前或
之前对A进行排序- 在线程发生之前,X和X线程发生在B之前。
同样,这些条件都没有得到满足,所以之前没有发生过线程。
第12段
评估A发生在评估B之前,如果:
- A在B之前排序,或
- 线程发生在B之前。
我们已经证明,在&#34;之前,两个操作都没有发生。另一个。术语&#34;在&#34;之前排序定义见1.9 / 13&#34;程序执行&#34;因为仅适用于在单个线程上发生的评估(&#34;在&#34之前排序;是C ++ 11&替换旧的&#34;序列点&#34;术语)。由于我们在单独的线程上讨论操作,因此在B之前无法对A进行排序。
所以在这一点上,我们发现iValue
没有&#34;发生在&#34;在另一个线程上发生的Cursor::Get()
修改(反之亦然)。最后,我们在第21段中找到了底线:
程序的执行包含数据竞争,如果它在不同的线程中包含两个冲突的动作,其中至少有一个不是原子的,并且都不会在另一个之前发生。任何此类数据竞争都会导致未定义的行为。
因此,如果您想在一个线程上使用iValue
并在另一个线程上使用修改volatile
的内容,则需要使用原子或其他一些排序操作(互斥或其他)来避免未定义的行为。
请注意,根据标准,volatile
不足以在线程之间提供排序。 Microsoft的编译器可以 为volatile
提供一些额外的承诺,以支持明确定义的线程间行为,但是这种支持是可配置的,所以我的建议是避免依赖{{1}用于新代码。以下是MSDN对此有何看法(http://msdn.microsoft.com/en-us/library/vstudio/12a04hfd.aspx):
符合ISO标准
如果您熟悉C#volatile关键字,或者熟悉早期版本的Visual C ++中volatile的行为,请注意C ++ 11 ISO标准的volatile关键字是不同的,并且当/ volatile:指定iso编译器选项。 (对于ARM,默认情况下指定)。 C ++ 11 ISO标准代码中的volatile关键字仅用于硬件访问;不要将它用于线程间通信。对于线程间通信,请使用C ++标准模板库中的std :: atomic等机制。
Microsoft特定
当使用/ volatile:ms编译器选项时 - 默认情况下,当ARM以外的体系结构成为目标时 - 除了维护对其他全局对象的引用的排序之外,编译器还会生成额外的代码来维护对volatile对象的引用之间的顺序。特别是:
对volatile对象的写入(也称为volatile write)具有Release语义;也就是说,在写入指令序列中的易失性对象之前发生的全局或静态对象的引用将发生在编译二进制文件中的易失性写入之前。
读取volatile对象(也称为volatile read)具有Acquire语义;也就是说,在读取指令序列中的易失性存储器之后发生的对全局或静态对象的引用将在编译二进制文件中的易失性读取之后发生。
这使得volatile对象可用于多线程应用程序中的内存锁定和释放。