C++ 11 memory_order_acquire和memory_order_release语义?

Ced*_*lja 25 c++ multithreading memory-model c++11

http://en.cppreference.com/w/cpp/atomic/memory_order和其他C++ 11在线参考,将memory_order_acquire和memory_order_release定义为:

  • 获取操作:在此加载之前,不能对当前线程中的读取进行重新排序.
  • 释放操作:在此存储之后,当前线程中的写入不能重新排序.

这似乎允许在获取操作之前执行获取后写入,这看起来很奇怪(通常的获取/释放操作语义限制所有内存操作的移动).

相同的在线资源(http://en.cppreference.com/w/cpp/atomic/atomic_flag)表明可以使用C++原子和上面提到的宽松内存排序规则构建自旋锁互斥:

lock mutex: while (lock.test_and_set(std::memory_order_acquire))

unlock mutex: lock.clear(std::memory_order_release);               
Run Code Online (Sandbox Code Playgroud)

有了这个锁定/解锁的定义,如果确实以这种方式定义了memory_order_acquire/release,那么下面的简单代码就不会被破坏(即,不禁止对获取后写入进行重新排序):

Thread1:
  (0) lock
    (1) x = 1;
    (2) if (x != 1) PANIC
  (3) unlock

Thread2:
  (4) lock
    (5) x = 0;
  (6) unlock
Run Code Online (Sandbox Code Playgroud)

以下执行是否可行:(0)锁定,(1)x = 1,(5)x = 0,(2)PANIC?我错过了什么?

Wan*_*gic 20

spinlock mutex实现对我来说没问题.我认为他们对获取释放的定义完全错误.

以下是我所知道的获取/释放一致性模型的最清晰解释:Gharachorloo; Lenoski; 劳顿; 吉本斯; 古普塔; Hennessy:可伸缩共享内存多处理器中的内存一致性和事件排序,Int'l Symp Comp Arch,ISCA(17):15-26,1990,doi 10.1145/325096.325102.(doi是ACM付费墙的后面.实际链接是不在付费墙后面的副本.)

请参阅第3.3节中的条件3.1和附图3:

  • 在允许对任何其他处理器执行普通加载或存储访问之前,必须执行所有先前的获取访问,并且
  • 在允许对任何其他处理器执行释放访问之前,必须执行所有先前的普通加载和存储访问,并且
  • 特殊访问[顺序]相互一致.

重点在于:获取和发布是顺序一致的(所有线程全局同意获取和发布的顺序.)所有线程全局同意在获取和特定线程上的发布之间发生的事情发生在获取之间并释放.但允许发布之后移动(通过硬件或编译器)正常的加载和存储,并且允许获取之后(通过硬件或编译器)将获取之前的正常加载和存储移动到获取之后.

C++标准中(我使用了2012年1月草案的链接),相关部分是1.10(第11到14页).

发生之前的定义旨在以Lamport为模型; 时间,时钟和分布式系统中事件的排序,CACM,21(7):558-565,1978年7月.C++ 获取对应Lamport的接收,C++ 版本对应Lamport的发送.Lamport在单个线程中对事件序列下了一个总顺序,其中C++必须允许部分顺序(参见章节1.9,第13-15段,第10页中的C++定义的序列 - 之前.)仍然,顺序 -订购之前几乎是你所期望的.语句按照程序中给出的顺序排序.第1.9节第14段:"每个值计算和与完整表达式相关的副作用在每个值计算和与要评估的下一个全表达式相关的副作用之前进行排序."

第1.10节的要点是说,一个无数据竞争的程序产生的定义值与在具有顺序一致的内存且没有编译器重新排序的机器上运行的程序相同.如果存在数据竞争,则程序根本没有定义的语义.如果没有数据竞争,则允许编译器(或机器)重新排序不会导致顺序一致性错觉的操作.

第1.10节第21段(第14页)说:如果从不同的线程到对象X有一对访问A和B,那么程序不是数据争用的,这些访问中至少有一个具有副作用,并且都没有A发生在B之前,B发生在A之前.否则该程序是无数据竞争的.

第6-20段对发生在之前的关系给出了非常仔细的定义.关键定义是第12段:

"评估A发生在评估B 之前,如果:

  • A在B之前排序,或
  • 线程发生在B之前."

因此,如果获取在之前(在同一个线程中)排序几乎任何其他语句,那么获取必须看起来发生在该语句之前.(包括该语句是否执行写入.)

同样地:如果几乎任何语句在一个版本之前(在同一个线程中)被排序,那么该语句必须在发布之前出现.(包括该语句是否只进行值计算(读取).)

编译器之所以允许在发行之前,释放后移至其他计算(或从acquire之前给acquire后),是因为事实上这些行动特别指出的是不是有一个线程间的关系之前发生(因为他们在关键部分之外).如果它们竞争,语义是未定义的,如果它们没有竞争(因为它们没有被共享),那么你无法确切知道它们何时发生在同步方面.

这是一个很长的说法:cppreference.com对获取和释放的定义是错误的.您的示例程序没有数据争用条件,并且不能发生PANIC.

  • 我将重写那个cppreference页面(因为我最初根据某人的博客笔记编写它有罪). (3认同)
  • @Cubbi:我认为应删除所有类型“在原子加载/存储之前/之后,读/写器线程中的所有读/写操作都不能重新排序”的表述。首先,我认为它们是错误的(例如,读取器中的读取可以在原子加载之前重新排序而不会破坏acq-rel语义)。其次,作为程序员,我不在乎重新排序编译器/硬件可能会做什么以提高性能,我关心的事实是,线程A中释放之前的语句(sequenced-)似乎会在(sequence- )在线程B完成获取之后。 (2认同)