自旋锁互斥锁实现对我来说看起来不错。我认为他们对获取和释放的定义完全错误。
这是我所知道的获取/发布一致性模型的最清晰的解释:Gharachorloo;列诺斯基;劳登;长臂猿;古普塔;Hennessy:可扩展共享内存多处理器中的内存一致性和事件排序,Int'l Symp Comp Arch,ISCA(17):15-26, 1990,doi 10.1145/325096.325102。(doi 在 ACM 付费墙后面。实际链接是指向不在付费墙后面的副本。)
查看第 3.3 节中的条件 3.1 和随附的图 3:
- 在允许对任何其他处理器执行普通加载或存储访问之前,必须执行所有先前的获取访问,并且
- 在允许对任何其他处理器执行释放访问之前,必须执行所有以前的普通加载和存储访问,并且
- 特殊访问 [sequentially] 彼此一致。
要点是:获取和释放是顺序一致的1(所有线程全局都同意获取和释放发生的顺序。)所有线程全局都同意在特定线程上的获取和释放之间发生的事情发生在获取和释放。但是允许将发布后的正常加载和存储(通过硬件或编译器)移动到发布之上,并且允许将获取之前的正常加载和存储(通过硬件或编译器)移动到获取之后.
(脚注 1:这对于大多数实现来说都是正确的,但是对于 ISO C++ 来说一般来说夸大了。读者线程可以不同意其他 2 个线程完成的 2 个存储的顺序。请参阅Acquire/release semantics with 4 threads和这个答案有关如何为 POWER CPU 编译 C++ 的详细信息,展示了在实践中与发布和获取的差异,但不是 seq_cst。但大多数 CPU 仅通过一致的缓存在内核之间获取数据,这意味着确实存在全局顺序。)
在C++ 标准中(我使用了 2012 年 1 月草案的链接),相关部分是 1.10(第 11 页到第 14 页)。
“ happens-before ”的定义意在仿照Lamport;分布式系统中的时间、时钟和事件顺序,CACM,21(7):558-565,1978 年 7 月。C++获取对应于 Lamport 的接收,C++发布对应于 Lamport 的发送。Lamport 对单个线程内的事件序列进行了总排序,其中 C++ 必须允许部分排序(参见第 1.9 节,第 13-15 段,第 10 页,了解 C++ 对sequenced-before的定义。)不过,sequenced-前订购几乎是您所期望的。语句按照它们在程序中给出的顺序排列。第 1.9 节,第 14 段:“与完整表达式相关的每个值计算和副作用都在与要评估的下一个完整表达式相关的每个值计算和副作用之前排序。”
1.10 节的重点是说,一个无数据竞争的程序产生相同的定义良好的值,就好像该程序在具有顺序一致内存且没有编译器重新排序的机器上运行一样。如果存在数据竞争,则程序根本没有定义的语义。如果没有数据竞争,则允许编译器(或机器)重新排序不会导致顺序一致性错觉的操作。
第 1.10 节,第 21 段(第 14 页)说:如果有一对从不同线程对对象 X 的访问 A 和 B,则程序不是无数据竞争的,至少其中一个访问有副作用,并且两者都没有A 发生在 B 之前,B 也不发生在 A 之前。否则程序是无数据竞争的。
第 6 至 20 段对happens-before 关系给出了非常仔细的定义。关键定义是第 12 段:
“如果满足以下条件,则评估A 发生在评估 B 之前:
- A 在 B 之前排序,或
- 线程间发生在 B 之前。”
因此,如果获取在几乎任何其他语句之前(在同一个线程中)排序,那么获取必须出现在该语句之前。(包括该语句是否执行写入。)
同样:如果几乎任何语句都在发布之前(在同一个线程中)排序,那么该语句必须出现在发布之前。(包括该语句是否只进行值计算(读取)。)
允许编译器将其他计算从发布之后移动到发布之前(或从获取之前到获取之后)的原因是因为这些操作在关系之前没有线程间发生(因为它们在临界区之外)。如果他们竞争语义是未定义的,并且如果他们不竞争(因为它们不是共享的),那么你就无法准确判断它们何时发生在同步方面。
这是一个很长的说法:cppreference.com 对获取和释放的定义是完全错误的。您的示例程序没有数据竞争条件,并且不会发生 PANIC。