C ++ 11引入了标准化的内存模型。这是什么意思?它将如何影响C ++编程?


1894

C ++ 11引入了标准化的内存模型,但这究竟意味着什么?它将如何影响C ++编程?

这篇文章(由加文·克拉克Gavin Clarke)引用赫伯·萨特Herb Sutter)表示)说,

内存模型意味着C ++代码现在有一个标准化的库可以调用,而不管编译器的创建者和运行平台是什么。有一种标准方法可以控制不同线程如何与处理器的内存通信。

“当你在谈论分裂[代码]在不同的内核,就是在标准,我们正在谈论的内存模型。我们要优化它没有打破以下假设人会在代码中做出,” 萨特说。

好吧,我可以记住这一段以及网上可以找到的类似段落(因为我从出生就拥有自己的记忆模型:P),甚至可以发布它作为对其他人提出的问题的答案,但是老实说,我并不完全理解这个。

C ++程序员甚至以前都曾开发过多线程应用程序,那么,它是POSIX线程,Windows线程还是C ++ 11线程又有什么关系呢?有什么好处?我想了解底层细节。

我还感觉到C ++ 11内存模型与C ++ 11多线程支持某种程度上相关,因为我经常将两者结合在一起。如果是的话,究竟如何?为什么要关联它们?

由于我不了解多线程的内部原理以及内存模型的一般含义,请帮助我理解这些概念。:-)


3
@curiousguy:详细...
Nawaz

4
@curiousguy:然后写一个博客...并提出修复建议。没有其他方法可以使您的观点正确和合理。
纳瓦兹

2
我把那个站点误认为是提问和交流想法的地方。我的错; 在这里,即使您赫伯·萨特(Herb Sutter)在投掷规格方面自相矛盾,您也无法不同意。
curiousguy19年

5
@curiousguy:C ++是标准所说的,而不是互联网上随意的人所说的。是的,必须符合标准。C ++不是一种开放的哲学,您可以在其中谈论任何不符合标准的事物。
纳瓦兹

3
“我证明了没有C ++程序可以具有明确定义的行为。” 。高大的索赔,没有任何证据!
纳瓦兹

Answers:


2204

首先,您必须学会像语言律师一样思考。

C ++规范未引用任何特定的编译器,操作系统或CPU。它引用了抽象机,它是对实际系统的概括。在语言律师界,程序员的工作是为抽象机编写代码。编译器的工作是在具体机器上实现该代码。通过严格按照规范进行编码,可以确定您的代码可以在不使用兼容C ++编译器的任何系统上进行编译和运行,而无论是现在还是50年后。

C ++ 98 / C ++ 03规范中的抽​​象机基本上是单线程的。因此,不可能编写相对于规范“完全可移植”的多线程C ++代码。规范甚至没有说关于内存加载和存储的原子性或加载和存储发生的顺序的任何事情,不用管互斥锁之类的事情。

当然,您可以在实践中为特定的具体系统(例如pthread或Windows)编写多线程代码。但是,没有为C ++ 98 / C ++ 03编写多线程代码的标准方法。

C ++ 11中的抽象机在设计上是多线程的。它还具有定义明确的内存模型;也就是说,它说明了编译器在访问内存时可能会做或可能不会做的事情。

考虑以下示例,其中两个线程同时访问一对全局变量:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

线程2可能输出什么?

在C ++ 98 / C ++ 03下,这甚至不是“未定义行为”;这个问题本身是没有意义的,因为该标准并未考虑任何称为“线程”的内容。

在C ++ 11下,结果是未定义行为,因为加载和存储通常不需要是原子的。看起来似乎并没有太大的改善...就其本身而言,不是。

但是,使用C ++ 11,您可以编写以下代码:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

现在事情变得更加有趣了。首先,在这里定义行为。现在可以打印线程2 0 0(如果它在线程1之前运行),37 17(如果它在线程1之后运行)或0 17(如果在线程1分配给x之后但又分配给y之后运行)。

它不能打印的是37 0,因为C ++ 11中原子加载/存储的默认模式是强制顺序一致性。这只是意味着所有加载和存储必须“好像”它们按照您在每个线程中写入它们的顺序进行,而线程之间的操作可以交错,但是系统喜欢。所以原子能的默认行为,同时提供了原子排序的加载和存储。

现在,在现代CPU上,确保顺序一致性可能很昂贵。特别是,编译器很可能在每次访问之间发出完全成熟的内存屏障。但是,如果您的算法可以容忍乱序的加载和存储;即,如果它需要原子性但不需要排序;即,如果它可以容忍37 0此程序的输出,则可以编写以下代码:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

CPU越现代,它比上一个示例更快的可能性就越大。

最后,如果只需要按顺序保留特定的装入和存储,则可以编写:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

这将我们带回到有序的装载和存储- 37 0不再是可能的输出-却以最小的开销做到了。(在这个简单的示例中,结果与成熟的顺序一致性相同;在较大的程序中,结果则不是。)

当然,如果要查看的唯一输出是0 037 17,则只需在原始代码周围包裹一个互斥体即可。但是,如果您已经读了那么多书,我敢打赌,您已经知道它是如何工作的,并且这个答案已经比我打算的要长:-)。

因此,底线。互斥体很棒,C ++ 11对其进行了标准化。但是有时出于性能原因,您需要较低级别的基元(例如,经典的双重检查锁定模式)。新标准提供了诸如互斥锁和条件变量之类的高级小工具,还提供了诸如原子类型和各种不同的内存屏障之类的低级小工具。因此,现在您可以完全使用标准指定的语言编写复杂的高性能并发例程,并且可以确定您的代码可以在当今和未来的系统上编译并运行不变。

坦率地说,除非您是专家并且致力于一些严肃的低级代码,否则您应该坚持使用互斥锁和条件变量。那就是我打算做的。

有关这些内容的更多信息,请参见此博客文章


37
很好的答案,但这确实是乞求一些新的原语的实际例子。另外,我认为没有原语的内存排序与C ++ 0x之前的版本相同:没有保证。
约翰·里普利

5
@John:我知道,但是我仍然在自己学习原语:-)。另外,我认为它们保证字节访问是原子访问(尽管不是有序的),这就是为什么我使用“ char”作为示例。但是,我什至对此也不是100%确信。如果您想提出任何好的“教程”参考资料,我会将它们添加到我的答案中
Nemo

48
@Nawaz:是的!内存访问可以由编译器或CPU重新排序。考虑(例如)缓存和推测性负载。系统内存命中的顺序可能与您编写的内容完全不同。编译器和CPU将确保此类重新排序不会破坏单线程代码。对于多线程代码,“内存模型”描述了可能的重新排序,以及两个线程同时读取/写入同一位置会发生什么情况,以及您如何控制这两个位置。对于单线程代码,内存模型无关紧要。
Nemo

26
@ Nawaz,@ Nemo-一个较小的细节:新的内存模型与单线程代码有关,因为它指定了某些表达式(例如)的不确定性i = i++序列点的旧概念已被废弃;新标准使用先序顺序关系指定了同一事物,这只是更一般的线程间先发生概念的特例。
JohannesD

17
@ AJG85:C ++ 0x规范草案的第3.6.2节说:“具有静态存储持续时间(3.7.1)或线程存储持续时间(3.7.2)的变量应在进行任何其他初始化之前进行零初始化(8.5)。地点。” 由于在此示例中x,y是全局的,因此它们具有静态的存储期限,因此我将对其进行零初始化。
Nemo

345

我将提供一个类比,以理解内存一致性模型(或简称为内存模型)。它受到Leslie Lamport的开创性论文“分布式系统中的时间,时钟和事件排序”的启发。这个比喻是恰当的,具有根本的意义,但对许多人来说可能是过大的杀伤力。但是,我希望它能提供一种心理图像(图形表示形式),以促进有关内存一致性模型的推理。

让我们在时空图中查看所有存储位置的历史记录,其中水平轴表示地址空间(即,每个存储位置由该轴上的一个点表示),垂直轴表示时间(我们将看到,通常,没有普遍的时间概念。因此,每个存储器位置保存的值的历史记录由该存储器地址处的垂直列表示。每个值更改都是由于其中一个线程将新值写入该位置而引起的。通过一个存储图像,我们将意味着所有的内存位置观察到的价值的总和/组合在特定的时间特定线程

引用“内存一致性和缓存一致性入门”

直观(且限制性最强)的内存模型是顺序一致性(SC),在该模型中,多线程执行应看起来像每个组成线程的顺序执行的交错,就像线程在单核处理器上是时分复用的一样。

该全局内存顺序可以从程序的一次运行到另一次运行而有所不同,并且可能事先未知。SC的特征是地址空间-时间图中的水平切片集,表示同时平面(即内存图像)。在给定平面上,其所有事件(或内存值)都是同时发生的。有一个绝对时间的概念,其中所有线程都同意哪些内存值是同时的。在SC中,每时每刻只有一个内存映像被所有线程共享。也就是说,在每个时刻,所有处理器都在内存映像(即内存的聚合内容)上达成一致。这不仅意味着所有线程对于所有内存位置均查看相同的值序列,而且还意味着所有处理器均观察到相同的值。所有变量的值组合。这与说所有线程以相同的总顺序观察所有内存操作(在所有内存位置)相同。

在宽松的内存模型中,每个线程将以自己的方式切分地址空间-时间,唯一的限制是每个线程的切面不得相互交叉,因为所有线程必须就每个单独的内存位置的历史达成共识(当然,不同线程的切片可能并且将彼此交叉)。没有通用的方式对其进行切片(没有特权的地址空间-时空组合)。切片不必是平面的(或线性的)。它们可以是弯曲的,这可以使一个线程以不同于其写入顺序的方式读取另一个线程写入的值。不同内存位置的历史记录在被任何特定线程查看时可能会相对于彼此任意滑动(或拉伸)。每个线程对同时发生的事件(或等效地,内存值)有不同的理解。与一个线程同时发生的事件(或内存值)集与另一个线程不同时。因此,在宽松的内存模型中,所有线程对于每个内存位置仍然遵循相同的历史记录(即值的序列)。但是他们可能观察到不同的内存映像(即所有内存位置的值的组合)。即使同一线程依次写入两个不同的内存位置,其他线程也可能以不同的顺序观察到这两个新写入的值。

[图片来自维基百科] 图片来自维基百科

熟悉爱因斯坦狭义相对论的读者会注意到我所暗示的内容。将Minkowski的话转化为内存模型领域:地址空间和时间是地址空间时间的影子。在这种情况下,每个观察者(即线程)将事件的阴影(即内存存储/加载)投影到他自己的世界线(即他的时间轴)和他自己的同时性平面(他的地址空间轴)上。C ++ 11内存模型中的线程对应于以相对论相对运动的观察者。顺序一致性对应于伽利略时空(即,所有观察者都同意一个事件的绝对顺序和全局的同时性)。

记忆模型与狭义相对论之间的相似之处源于以下事实:两者都定义了部分有序的事件集,通常称为因果集。一些事件(即内存存储)可以影响(但不受其他事件影响)。C ++ 11线程(或物理学中的观察者)不过是事件(例如,内存加载和存储到可能不同的地址)的链(即,完全有序的集合)。

相对而言,由于所有观察者都同意的唯一时间顺序是“类似时间”的事件之间的顺序(即,原则上可以被任何变慢的粒子连接的那些事件),因此将部分顺序恢复到看似混乱的部分顺序事件的画面上比真空中的光速)。仅与时间相关的事件是不变排序的。 物理时间,克雷格·卡伦德(Craig Callender)

在C ++ 11内存模型中,使用类似的机制(获取-发布一致性模型)来建立这些局部因果关系

为了提供内存一致性的定义和放弃SC的动机,我将引用“内存一致性和缓存一致性入门”。

对于共享内存机器,内存一致性模型定义其内存系统在体系结构上可见的行为。单个处理器内核的正确性标准将行为划分为“ 一个正确的结果 ”和“ 许多不正确的选择 ”之间。这是因为处理器的体系结构要求,即使在无序的内核上,线程的执行也可以将给定的输入状态转换为定义良好的单个输出状态。但是,共享内存一致性模型涉及多个线程的负载和存储,并且通常允许许多正确的执行同时禁止很多(更多)不正确的代码。多次正确执行的可能性归因于ISA允许多个线程同时执行,并且通常可能对来自不同线程的指令进行许多合法的交织。

宽松弱的内存一致性模型是由以下事实引起的:强模型中的大多数内存排序都是不必要的。如果一个线程先更新十个数据项,然后再更新一个同步标志,则程序员通常不在乎数据项是否按顺序进行更新,而只是在更新标志之前仅更新所有数据项(通常使用FENCE指令实现) )。宽松的模型试图捕获这种增加的订购灵活性,并仅保留程序员“需要的订单””,以获得更高的性能和SC的正确性。例如,在某些架构中,每个内核使用FIFO写缓冲区来保存已提交(已退休)存储的结果,然后再将结果写入高速缓存。此优化可提高性能,但会违反SC。写缓冲区隐藏了为存储未命中服务的等待时间。由于商店很常见,因此能够避免大多数商店停滞是一个重要的好处。对于单核处理器,即使写入A的一个或多个存储在A中,也可以通过确保对地址A的加载将最新存储的值返回A来使写入缓冲区在体系结构上不可见。通常,这是通过以下方式完成的:要么将最新存储的值绕过A到A,再从A加载,其中“最新”由程序顺序确定,或者如果写缓冲区中存储有A,则通过停止A的负载来完成。当使用多个内核时,每个内核都有自己的旁路写缓冲区。如果没有写缓冲区,则硬件是SC,但是没有写缓冲区,则是硬件,这使得写缓冲区在多核处理器中在体系结构上可见。

如果内核具有一个非FIFO写缓冲区,可以使存储以与输入顺序不同的顺序离开,则可能会发生存储-存储重排序。如果第一个存储在第二个命中时丢失了高速缓存,或者第二个存储可以与一个较早的存储合并(即在第一个存储之前),则可能会发生这种情况。负载重排序也可能发生在动态调度的内核上,这些内核以非程序顺序执行指令。这可以与对另一个内核上的存储进行重新排序的行为相同(您能否提出一个在两个线程之间进行交织的示例?)。使用较晚的存储区对较早的加载进行重新排序(对存储区进行重新排序)会导致许多不正确的行为,例如在释放保护它的锁之后加载值(如果存储区是解锁操作)。

因为有时会混淆高速缓存一致性和内存一致性,所以也有这样的引用是有启发性的:

与一致性不同,缓存一致性对于软件既不可见也不是必需的。Coherence试图使共享内存系统的缓存在功能上与单核系统中的缓存一样不可见。正确的一致性可确保程序员无法通过分析加载和存储的结果来确定系统是否以及在何处具有缓存。这是因为正确的一致性可确保高速缓存永远不会启用新的或不同的功能行为(程序员可能仍能够使用时序来推断可能的高速缓存结构信息)。高速缓存一致性协议的主要目的是使每个存储位置的单写多读器(SWMR)保持不变。一致性和一致性之间的重要区别在于,一致性是在每个内存位置的基础上指定的,而一致性是针对所有内存位置指定的。

继续我们的心理图景,SWMR不变性对应于物理要求,即在任一位置最多存在一个粒子,但在任何位置都可以有无限数量的观察者。


52
对于具有相对论的类比+1,我一直在尝试自己做一个类比。我经常看到程序员正在研究线程代码,试图将行为解释为不同线程中的操作以特定顺序相互交错发生,我不得不告诉他们,不,对于多处理器系统,不同的<s之间是同时存在的。 >参考框架</ s>线程现在变得毫无意义。与狭义相对论是使他们尊重问题复杂性的好方法。
Pierre Lebeaupin 2014年

71
那么您是否应该得出结论,宇宙是多核的呢?
彼得·K

6
@PeterK:完全是:)这是物理学家布莱恩·格林(Brian Greene)对时间的生动描述:youtube.com/watch? v=4BjGWLJNPcA&t=22m12s 这是“时间的错觉[完整纪录片]”,第22分钟, 12秒
艾哈迈德·纳萨尔

2
是我还是他正在从1D内存模型(水平轴)切换到2D内存模型(同时平面)。我觉得这有点令人困惑,但这也许是因为我不是母语人士……还是很有趣的读物。
再见SE

您忘记了一个基本部分:“ 通过分析负载和存储结果 ”,而无需使用精确的时序信息。
curiousguy19年

115

这是一个已有多年历史的问题,但是它非常受欢迎,值得一提的是一个很棒的资源,可以用来学习C ++ 11内存模型。我想总结一下他的演讲以给出另一个完整的答案没有任何意义,但是鉴于这实际上是编写标准的人,我认为值得一听。

Herb Sutter讨论了名为“ atomic <>武器”的C ++ 11内存模型,历时3小时,可在Channel9网站的第1 部分第2部分中获得。该演讲是非常技术性的,涵盖以下主题:

  1. 优化,竞争和内存模型
  2. 订购–内容:获取并发布
  3. 订购–操作方式:互斥体,原子和/或栅栏
  4. 编译器和硬件的其他限制
  5. 代码生成和性能:x86 / x64,IA64,POWER,ARM
  6. 弛豫原子

讨论的内容不是在API上,而是在幕后和幕后的推理,背景(您是否知道宽松的语义被添加到标准中是因为POWER和ARM不有效地支持同步负载?)。


10
那个演讲确实很棒,完全值得您花3个小时观看。
ZunTzu

5
@ZunTzu:在大多数视频播放器上,您可以将速度设置为原始速度的1.25、1.5甚至2倍。
克里斯蒂安·塞弗林2015年

4
@eran你们碰巧有幻灯片吗?频道9对话页面上的链接不起作用。
athos '16

2
@athos我没有它们,对不起。尝试联系第9频道,我认为删除不是故意的(我想他们是从Herb Sutter获得链接的,照原样发布,后来他删除了文件;但这只是一种推测...)。
eran

75

这意味着该标准现在定义了多线程,并且定义了在多线程的上下文中发生的情况。当然,人们使用了不同的实现,但这就像在问为什么我们应该有一个std::string我们都可以使用家庭学习string班的时候。

当您谈论POSIX线程或Windows线程时,这实际上是您在谈论x86线程,这是一种错觉,因为它是同时运行的硬件功能。C ++ 0x内存模型可以保证,无论您使用的是x86,ARM,MIPS还是其他任何东西。


28
Posix线程不限于x86。实际上,在其上实现的第一个系统可能不是x86系统。Posix线程与系统无关,并且在所有Posix平台上均有效。这并不是一个真正的硬件属性,因为Posix线程也可以通过协作式多任务处理来实现。但是,当然,大多数线程问题仅出现在硬件线程实现上(有些甚至只出现在多处理器/多核系统上)。
celtschk

57

对于未指定内存模型的语言,您正在为处理器架构指定的语言内存模型编写代码。处理器可以选择重新排序内存访问以提高性能。因此,如果您的程序有数据争用(数据争用是指多个内核/超线程有可能同时访问同一内存),则您的程序就不会跨平台,因为它依赖处理器内存模型。您可以参考Intel或AMD软件手册,以了解处理器如何重新排序内存访问。

非常重要的是,锁(以及带有锁的并发语义)通常以跨平台的方式实现...因此,如果您在没有数据争用的多线程程序中使用标准锁,则不必担心跨平台内存模型

有趣的是,用于C ++的Microsoft编译器已经获取/发布了volatile语义,这是C ++的扩展,可以解决C ++中缺少内存模型的问题http://msdn.microsoft.com/zh-cn/library/12a04hfd(v=vs .80).aspx。但是,考虑到Windows仅在x86 / x64上运行,这没什么可说的(Intel和AMD内存模型可以轻松,高效地实现一种语言的获取/释放语义)。


2
的确,在编写答案时,Windows仅在x86 / x64上运行,但是Windows有时会在IA64,MIPS,Alpha AXP64,PowerPC和ARM上运行。如今,它可以在各种版本的ARM上运行,这在内存方面与x86完全不同,并且几乎没有什么可以容忍的。
洛伦佐·德玛特(LorenzoDematté)2016年

该链接已损坏(例如“ Visual Studio 2005退休的文档”)。想要更新吗?
Peter Mortensen

3
即使写出答案也不是真的。

以并发方式访问同一内存 ”以一种冲突的方式进行访问
curiousguy18年

27

如果您使用互斥锁来保护所有数据,则实际上不必担心。互斥锁始终提供足够的顺序和可见性保证。

现在,如果您使用原子或无锁算法,则需要考虑内存模型。内存模型精确地描述了原子何时提供排序和可见性保证,并提供了用于手动编码保证的便携式围栏。

以前,原子将使用编译器内部函数或更高级别的库来完成。可以使用特定于CPU的指令(内存屏障)来完成防护。


19
以前的问题是没有互斥体(根据C ++标准)。因此,唯一提供的保证是互斥体制造商提供的,只要您不移植代码就可以了(因为很难发现对保证的微小更改)。现在,我们获得了该标准提供的保证,该保证应可在平台之间移植。
马丁·约克

4
@Martin:无论如何,一件事是内存模型,另一件事是在该内存模型之上运行的原子和线程原语。
ninjalj 2011年

4
另外,我的观点主要是以前在语言级别上几乎没有内存模型,而恰好是底层CPU的内存模型。现在有一个内存模型,它是核心语言的一部分;OTOH,互斥锁等始终可以作为库来完成。
ninjalj 2011年

3
对于试图编写互斥库的人来说,这也可能是一个真正的问题。当CPU,内存控制器,内核,编译器和“ C库”全部由不同的团队实现时,其中一些人在关于这些东西应该如何工作方面存在激烈的分歧,好吧,有时是这些东西我们的系统程序员必须要做的是向应用程序级别展示漂亮的外观,这一点都不愉快。
zwol 2011年

11
不幸的是,如果您的语言中没有一致的内存模型,仅用简单的互斥体来保护数据结构是不够的。有多种编译器优化在单线程上下文中有意义,但是当多个线程和cpu内核发挥作用时,内存访问的重新排序和其他优化可能会产生不确定的行为。有关更多信息,请参见Hans Boehm的“无法将线程实现为库”:citeseer.ist.psu.edu/viewdoc/…– exDM69 2011
6

0

上面的答案涉及C ++内存模型的最基本方面。在实践中,std::atomic<>至少在程序员过度优化之前(例如,通过尝试放松过多的事情),大多数情况下都使用“正常工作”。

在一个地方,错误仍然很常见:序列锁https://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf上有关于挑战的精彩且易于阅读的讨论。顺序锁很吸引人,因为读者可以避免写入锁定字。以下代码基于以上技术报告的图1,并且突出显示了在C ++中实现序列锁时的挑战:

atomic<uint64_t> seq; // seqlock representation
int data1, data2;     // this data will be protected by seq

T reader() {
    int r1, r2;
    unsigned seq0, seq1;
    while (true) {
        seq0 = seq;
        r1 = data1; // INCORRECT! Data Race!
        r2 = data2; // INCORRECT!
        seq1 = seq;

        // if the lock didn't change while I was reading, and
        // the lock wasn't held while I was reading, then my
        // reads should be valid
        if (seq0 == seq1 && !(seq0 & 1))
            break;
    }
    use(r1, r2);
}

void writer(int new_data1, int new_data2) {
    unsigned seq0 = seq;
    while (true) {
        if ((!(seq0 & 1)) && seq.compare_exchange_weak(seq0, seq0 + 1))
            break; // atomically moving the lock from even to odd is an acquire
    }
    data1 = new_data1;
    data2 = new_data2;
    seq = seq0 + 2; // release the lock by increasing its value to even
}

由于直观的,因为它在接缝首先,data1data2需要是atomic<>。如果它们不是原子的,则可以在reader()写入它们的同时(在中)读取它们writer()。根据C ++内存模型,即使reader()从不实际使用data,这也是一场竞赛。另外,如果它们不是原子的,则编译器可以将每个值的第一次读取缓存在寄存器中。很显然,你不会想...你想在每次迭代重新读取while的循环reader()

制作它们atomic<>并使用访问也是不够的memory_order_relaxed。这样做的原因是seq(in中reader())的读取仅具有获取语义。简单来说,如果X和Y是内存访问,X在Y之前,X不是获取或释放,并且Y是获取,那么编译器可以在X之前对Y进行重新排序。如果Y是seq的第二次读取,并且X如果是读取数据,则这种重新排序将破坏锁的实现。

本文提供了一些解决方案。今天性能最好的一个可能是在seqlock的第二次读取之前使用atomic_thread_fencewith 的那个。在本文中,它是图6。在这里,我不会在这里复制代码,因为到目前为止已经读过本文的人都应该阅读本文。它比这篇文章更加精确和完整。memory_order_relaxed

最后一个问题是使data变量成为原子可能是不自然的。如果您不能在代码中使用,则需要非常小心,因为从非原子转换为原子仅对原始类型合法。C ++ 20应该添加atomic_ref<>,这将使此问题更易于解决。

总结一下:即使您认为自己了解C ++内存模型,在滚动自己的序列锁之前也应该非常小心。


-2

C和C ++过去是由格式良好的程序的执行跟踪定义的。

现在,它们一半是由程序的执行跟踪定义的,另一半是由对同步对象的许多排序的后验的。

这意味着这些语言定义完全没有意义,因为没有逻辑方法来混合这两种方法。特别是,互斥锁或原子变量的销毁定义不充分。


我与您一样强烈希望改善语言设计,但是我认为,如果以简单案例为中心,您的答案将更有价值,对于此案例,您清楚明确地表明了这种行为是如何违反特定语言设计原则的。从那以后,我会强烈建议你,如果你让我,在这个问题的答案给出的每个点的相关性很好的论证,因为他们将反对由C ++设计感知的inmense生产效益的相关性进行对比
马蒂亚斯Haeussler

1
@MatiasHaeussler我想你误解了我的答案;我在这里不反对特定C ++功能的定义(我也有很多这样的针对性批评,但在这里不是)。我在这里争论的是,在C ++(也没有C)中没有明确定义的构造。整个MT语义是一团糟,因为您不再具有顺序语义。(我相信Java MT是坏的,但更少。)“简单的例子”几乎是任何MT程序。如果您不同意,欢迎回答我有关如何证明MT C ++程序正确性的问题。
curiousguy19年

有趣的是,我想我在读完您的问题之后会理解您的意思。如果我是正确的话,那是指不可能为C ++ MT程序的正确性开发证明。在这种情况下,我想说对我来说对于计算机编程的未来,尤其是对人工智能的到来至关重要。但我也要指出,对于大多数人来说,即使在理解了你的意思并变得感兴趣之后,他们甚至都不知道堆栈溢出问题,这甚至是他们所不知道的
Matias Haeussler

1
“有关计算机程序可扩展性的问题是否应该在stackoverflow或stackexchange中发布(如果都不在哪里,在哪里)?” 这个似乎是元堆栈溢出的一个,不是吗?
Matias Haeussler

1
@MatiasHaeussler 1)C和C ++本质上共享原子变量,互斥锁和多线程的“内存模型”。2)与此相关的是拥有“内存模型”的好处。我认为该模型不完善,因此收益为零。
curiousguy19年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.