C++ memory model - does this example contain a data race?
我正在阅读Bjarne Stroustrup的C++ 11 FAQ,而且我很难理解内存模型中的一个例子。
他给出了以下代码片段:
1 2 3
| // start with x==0 and y==0
if (x) y = 1; // thread 1
if (y) x = 1; // thread 2 |
常见问题解答说这里没有数据竞赛。我不明白。内存位置x由线程1读取,由线程2写入,而不进行任何同步(y也是如此)。这是两个访问,其中一个是写。这不是数据竞赛的定义吗?
此外,它还说:"当前的C++编译器(我知道)给出了一个正确答案。"这个正确答案是什么?根据一个线程的比较发生在另一个线程的写入之前还是之后(或者如果另一个线程的写入对读取线程甚至可见),答案是否会有所不同?
1 2 3
| // start with x==0 and y==0
if (x) y = 1; // thread 1
if (y) x = 1; // thread 2 |
因为x和y都不是真的,另一个也不会设置为真。无论指令的执行顺序如何,(正确的)结果总是x保持0,y保持0。
- 这是一个困难的问题:d
- 我想其中一个变量可以在另一个地方改变。^ ^
- 现在看来很明显。我想得太多了。
- 这个(公认的)答案是错误的。这不是数据争用,因为使用该符号,内存访问是顺序一致的,因此存在同步机制。但是,x=1和y=1的结果是不合法的,因为这会使svr-pes20-cppm.cl.cam.ac.uk/cppm/…谓词失败,这意味着对x的写操作不可能是对x的读操作的可见副作用,对y的写操作不可能同时是对y的读操作的可见副作用,因为至少必须进行一次读操作。在它相应的书写之前
- 如果您将代码更改为if(x.load(std::memory_order_released))等,执行所有的加载和存储,那么这个代码确实有数据竞争,因为在这种情况下确实没有同步机制。
- @卡洛伍德"那么这个密码"你能证明吗?
- 在这种情况下,很容易(无论如何都有必要的知识)。线程1读取X,线程2写入X。没有同步机制(互斥或顺序一致性),因此存在数据争用。最值得注意的是,对于这种推理,有一个if()根本不重要。无论有没有if()的推理都是一样的,并且测试什么都不重要(好吧,可能除了if(0),因为编译器根本不会生成if体)。
The memory location x is ... written to by thread 2
真的吗?你为什么这么说?
如果y为0,那么线程2不会将x写入。y从0开始。同样,除非在线程1运行之前,y不为零,否则x不能为非零,而这是不可能发生的。这里的一般观点是,不执行的条件写入不会导致数据争用。
不过,这是内存模型的一个重要事实,因为不知道线程的编译器可以(假设y不是易失性的)将代码if (x) y = 1;转换为int tmp = y; y = 1; if (!x) y = tmp;。然后会有一场数据竞赛。我无法想象它为什么要做这种精确的转换,但这并不重要,关键是非线程环境的优化器可以做一些违反线程内存模型的事情。因此,当Stroustrup说他知道的每一个编译器都给出正确的答案(恰好在C++ 11的线程模型下),那是一个关于C++ 11线程的编译器的准备的非平凡的声明。
更现实的if (x) y = 1的转变将是y = x ? 1 : y;。我相信在您的例子中,这将导致数据竞争,并且对于分配标准y = y中没有特殊的处理,使得在另一个线程中对y的读取执行未排序是安全的。你可能会发现很难想象它在哪个硬件上不工作,不管怎样,我可能错了,这就是为什么我使用了上面的另一个例子,它不太现实,但有一个明显的数据竞争。
- 为了使CPU能够处理合理编译的Java代码,它必须保证对内存地址的冲突访问被明确定义并且不产生随机值,因此,如果一个线程在一个循环中执行EDOCX1×0(在原子内存位置),而另一个线程执行EDCOX1(1),则没有线程创建原始值。(噢,我不太确定,在某些奇怪的情况下,a = b==1?1:b永远不能"凭空"创造价值1。)
必须对写入进行完全排序,因为在其他线程首次将1写入变量之前,任何线程都不能写入变量x或y。换言之,您基本上有三种不同的场景:
线程1开始写入y,因为x是在if语句之前的某个时间点写入的,然后如果线程2随后出现,它会将x的相同值写入1,并且不会改变它以前的1值。
线程2开始写入x,因为y在if语句之前的某个时间点发生了更改,然后线程1将写入y,如果以后的值与1的值相同。
如果只有两个线程,那么会跳过if语句,因为x和y保持为0。
- 对不起,这种推理是无效的。它不是这样工作的;为了检查线程代码是否具有标准内存模型定义的未定义行为(aka,有数据竞争或其他一些问题),如果没有顺序一致性(有一个原因使其成为默认值;否则事情就是如此相反的,Intu)99.9%的程序员会出错)。由于这个程序中的加载和存储是默认的,因此seq cst不存在争用——但是您的特定推理是不正确的。
- @卡洛伍德,你能澄清一下吗?我描述的三个场景中,哪一个是错误的?正如我最初所说,写入必须有一个完整的顺序,一个写入不能在一个线程中以某个顺序出现,而在第二个线程中以另一个顺序出现。因此,当一个线程到达这个代码分支时,先前对x、y和这两个变量的写入,或者对当前执行的线程都不可见的这两个变量的写入。
- 顶部的第一行是一个错误的假设(aka,"必须有写入的总顺序,因为在其他线程第一次将1写入变量之前,没有线程可以写入变量x或y")。这似乎合乎逻辑,但不是内存模型的工作方式。不过,这个评论空间太短,无法解释如何准确地解释这一点。基本上,您需要制作一个图,其中节点被读取和写入所涉及的变量,并且(定向的)边表示某些关系。然后内存模型描述了哪个图是
- 有效且无效,这意味着只有有效的图才能在编译器+硬件保证内存模型的情况下发生。如果两个读取都是轻松的,那么它们都从另一个图中读取1的写入的图是有效的(例如,如果在实践中会发生这种情况,则肯定不会在x86硬件上发生)。以下是我的"memorymodel"程序在本例中的输出:gyazo.com/f64a5706eb2aa4203f2254c716f35cc希望链接在一段时间内保持有效;)。(使用github.com/carlowood/memorymodel制作)。
- 您也可以在svr-pes20-cppmem.cl.cam.ac.uk/cppm在线测试它,因为这个案例并不特别需要。
- @卡洛伍德感谢链接到记忆模型测试。但是,当我查看svr-pes20-cppmem.cl.cam.ac.uk/cppm,并将Execution 1(执行1/8)设置为Standard(标准)时,我仍然觉得每个场景中的写入都有一个"总顺序",即对于执行给定线程的给定处理器核心,写入的可见性是在给定的执行点排序的。当一个写入对该线程/核心可见时,它在执行期间保持可见,它的可见性不是可变的,对于原子值也不是局部的。这样,您就可以为写操作写出一个"顺序"。
- "标准"和"首选"没有区别。只是后者更快(并且在数学上证明与标准给出的更正式的公式相当)。另外,不同的线程可以看到以不同的顺序写入不同的内存位置,除非这些写入是顺序一致的。在我们的例子中,它们是(我没有看到任何改变的理由),但是当两个读取都放松时,仍然可以得到结果x==y==1。如图所示:gyazo.com/567aae14592e1b68a02ce03af4bbfd1d只有3个一致的执行。
- 与C++内存模型一致。也就是说,只有这三种执行(或图形)才能发生。第一个具有x==y==0,这是微不足道的——第二个显示在屏幕截图中,如您所见具有x==y==1,第三个是相同的,除了橙色sc(顺序一致)箭头指向另一个方向;这就是两个线程看到写入顺序的方式。我理解这是非常违反直觉的,尽管这个图中的顺序是已知的/固定的,但结果仍然是x==y==1,但事实就是如此。你得看一下巴里的论文并研究它
- 像我所做的,在你明白这是标准允许的几个星期之前:这绝对是违反直觉的。在编写了我自己的"memorymodel"程序之后,我有了一个可以在这里使用的见解:如果你遵循所有的sb(之前排序)和rf(从中读取)边,那么如果你找到一个循环,就可能会有一致性问题(毕竟,两个边都包含"之前发生"的感觉,因此循环可能是因果关系冲突)。然而,如果在这个循环中,至少有两个不同的变量是可以轻松读取的,那么这个循环就不是因果关系冲突。就是这样
- @卡洛伍德,你能分享一下你提到的论文的链接吗?我明白你所说的,在所有线程的组合中,没有一个"总顺序",我认为这就是我们互相超越的地方……是的,"总顺序"只适用于给定的线程,两个不同的线程可能具有不同的可见写入顺序。
- cl.cam.ac.uk/~pes20/cpp/popl085ap-sewell.pdf我没有读过这本书,但它比较新,可能涵盖了很多相同的内容(?):cl.cam.ac.uk/~kn307/2016/…
两种写入都不会发生,因此没有竞争。X和Y都保持为零。
(这是关于幽灵写作的问题。假设有一个线程在检查条件之前进行了推测性的写入,然后尝试在之后进行更正。这会破坏另一条线,所以不允许这样做。)
内存模型设置代码和数据区域的可支持大小,在比较链接源代码之前,需要指定内存模型,即可以设置数据和代码的大小限制。