ChatGPT解决这个技术问题 Extra ChatGPT

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

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

This article(由 Gavin Clarke 引用 Herb Sutter)说,

内存模型意味着 C++ 代码现在有一个标准化的库可以调用,而不管编译器是谁制作的,也不管它在什么平台上运行。有一种标准方法可以控制不同线程如何与处理器的内存通信。 “当您谈论在标准中的不同内核之间拆分 [代码] 时,我们正在谈论内存模型。我们将在不破坏人们将在代码中做出的以下假设的情况下对其进行优化,”萨特说。

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

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

我也觉得 C++11 内存模型在某种程度上与 C++11 多线程支持有关,因为我经常看到这两者在一起。如果是,具体是怎样的?为什么它们应该相关?

我不知道多线程的内部是如何工作的,以及内存模型的一般含义。

@curiousguy:然后写一个博客……并提出修复建议。没有其他方法可以使您的观点有效和合理。
我把那个网站误认为是提问和交流想法的地方。我的错;即使 Herb Sutter 在投掷规格方面公然自相矛盾,您也不能不同意 Herb Sutter 的意见。
@curiousguy:C++ 是标准所说的,而不是互联网上随便一个人所说的。所以是的,必须符合标准。 C++ 不是一种开放的哲学,您可以在其中谈论任何不符合标准的事情。
“我证明了没有 C++ 程序可以有明确定义的行为。”。高大上的说法,没有任何证据!
不,我没有删除任何问题或答案。无论如何,原语有一定的保证,对吧?如果是这样,那么您可以在这些原始保证的基础上构建更大的保证。无论如何,您认为这只是 C++(也可能是 C)中的问题,还是所有语言中的问题?

N
Nemo

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

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 下,这甚至不是 Undefined Behavior;这个问题本身是没有意义的,因为该标准没有考虑任何称为“线程”的东西。

在 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 中原子加载/存储的默认模式是强制顺序一致性。这只是意味着所有加载和存储必须“好像”它们按照您在每个线程中编写它们的顺序发生,而线程之间的操作可以交错,但系统喜欢。因此,原子的默认行为为加载和存储提供了 atomicityordering

现在,在现代 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 将它们标准化。但有时出于性能原因,您需要较低级别的原语(例如,经典的 double-checked locking pattern)。新标准提供了诸如互斥锁和条件变量之类的高级工具,它还提供了诸如原子类型和各种形式的内存屏障之类的低级工具。因此,现在您可以完全使用标准指定的语言编写复杂、高性能的并发例程,并且您可以确定您的代码将在今天和明天的系统上编译和运行不变。

虽然坦率地说,除非您是专家并且正在处理一些严肃的低级代码,否则您可能应该坚持使用互斥锁和条件变量。这就是我打算做的。

有关这些内容的更多信息,请参阅 this blog post


很好的答案,但这真的是在乞求一些新原语的实际例子。另外,我认为没有原语的内存排序与 C++0x 之前的相同:没有保证。
@Nawaz:是的!内存访问可以由编译器或 CPU 重新排序。考虑(例如)缓存和推测性负载。系统内存被命中的顺序与您编码的完全不同。编译器和 CPU 将确保此类重新排序不会破坏单线程代码。对于多线程代码,“内存模型”描述了可能的重新排序,如果两个线程同时读/写同一位置会发生什么,以及如何对两者进行控制。对于单线程代码,内存模型无关紧要。
@Nawaz,@Nemo - 一个小细节:新的内存模型在单线程代码中是相关的,因为它指定了某些表达式的未定义性,例如 i = i++序列点的旧概念已被丢弃;新标准使用 sequenced-before 关系指定相同的事情,这只是更一般的线程间 happens-before 概念的特例。
@AJG85:C++0x 规范草案的第 3.6.2 节说,“具有静态存储持续时间 (3.7.1) 或线程存储持续时间 (3.7.2) 的变量应在进行任何其他初始化之前进行零初始化 (8.5)地方。”由于 x,y 在此示例中是全局的,因此我相信它们具有静态存储持续时间,因此将进行零初始化。
@Bemipefe:不,编译器没有义务按照您编写代码的顺序翻译您的代码 - 只要整体效果相同,就可以重新排序操作。例如,它可能会这样做,因为重新排序允许它生成更快(或更小)的代码。
C
Community

我将给出我理解内存一致性模型(或简称内存模型)的类比。它的灵感来自 Leslie Lamport 的开创性论文 "Time, Clocks, and the Ordering of Events in a Distributed System"。这个类比很贴切,具有根本意义,但对许多人来说可能有点矫枉过正。但是,我希望它提供一个心理图像(图形表示),有助于推理内存一致性模型。

让我们在时空图中查看所有内存位置的历史,其中横轴表示地址空间(即每个内存位置由该轴上的一个点表示),纵轴表示时间(我们将看到,一般来说,没有一个普遍的时间概念)。因此,每个内存位置保存的值的历史由该内存地址处的垂直列表示。每个值更改都是由于其中一个线程将新值写入该位置。内存映像是指特定线程在特定时间可观察到的所有内存位置的值的聚合/组合。

引自 "A Primer on Memory Consistency and Cache Coherence"

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

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

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

https://upload.wikimedia.org/wikipedia/commons/f/f1/Relsim2.GIF

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

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

在相对论中,部分有序事件的看似混乱的画面恢复了某种秩序,因为所有观察者都同意的唯一时间顺序是“类时间”事件之间的排序(即原则上可以通过任何速度变慢的粒子连接的那些事件)比真空中的光速还要快)。只有类时相关的事件是不变排序的。 Time in Physics, Craig Callender

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

为了提供内存一致性的定义和放弃 SC 的动机,我将引用 "A Primer on Memory Consistency and Cache Coherence"

对于共享内存机器,内存一致性模型定义了其内存系统的架构可见行为。单个处理器核心的正确性标准将行为划分为“一个正确的结果”和“许多不正确的选择”。这是因为处理器的架构要求线程的执行将给定的输入状态转换为单个明确定义的输出状态,即使在无序内核上也是如此。然而,共享内存一致性模型涉及多个线程的加载和存储,通常允许许多正确的执行,而不允许许多(更多)不正确的执行。多次正确执行的可能性是由于 ISA 允许多个线程同时执行,通常具有来自不同线程的许多可能的合法指令交错。宽松或弱内存一致性模型的动机是强模型中的大多数内存排序都是不必要的。如果一个线程更新了十个数据项,然后更新了一个同步标志,程序员通常不关心数据项是否按顺序更新,而只关心在更新标志之前更新所有数据项(通常使用 FENCE 指令实现)。宽松模型试图捕捉这种增加的排序灵活性,并只保留程序员“需要”以获得更高性能和 SC 正确性的命令。例如,在某些架构中,每个内核使用 FIFO 写入缓冲区来保存提交(退休)存储的结果,然后再将结果写入缓存。这种优化提高了性能,但违反了 SC。写缓冲区隐藏了服务存储未命中的延迟。因为商店很常见,所以能够避免在大多数商店中停滞不前是一个重要的好处。对于单核处理器,通过确保对地址 A 的加载将最近存储的值返回给 A,即使对 A 的一个或多个存储在写缓冲区中,也可以使写缓冲区在架构上不可见。这通常通过将最近存储到 A 的值绕过到从 A 加载来完成,其中“最近”由程序顺序确定,或者如果到 A 的存储在写缓冲区中,则停止加载 A .当使用多个内核时,每个内核都有自己的旁路写入缓冲区。没有写缓冲区,硬件就是 SC,但有了写缓冲区,它就不是了,这使得写缓冲区在多核处理器中在架构上是可见的。如果一个内核有一个非 FIFO 写缓冲区,它允许存储以不同于它们进入的顺序离开的顺序,则存储-存储重新排序可能会发生。如果第一个存储在缓存中未命中而第二个命中,或者如果第二个存储可以与较早的存储合并(即,在第一个存储之前),则可能会发生这种情况。加载-加载重新排序也可能发生在动态调度的内核上,这些内核以程序顺序执行指令。这与在另一个核心上重新排序存储的行为相同(你能想出一个在两个线程之间交错的示例吗?)。将较早的加载与较晚的存储重新排序(加载-存储重新排序)可能会导致许多不正确的行为,例如在释放保护它的锁之后加载一个值(如果存储是解锁操作)。请注意,存储加载重新排序也可能由于通常实现的 FIFO 写缓冲区中的本地旁路而出现,即使内核按程序顺序执行所有指令也是如此。

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

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

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


+1 对于狭义相对论的类比,我一直在尝试自己做同样的类比。我经常看到程序员研究线程代码,试图将行为解释为不同线程中的操作以特定顺序相互交错发生,我必须告诉他们,不,对于多处理器系统,不同 参考框架线程现在毫无意义。与狭义相对论进行比较是让他们尊重问题复杂性的好方法。
那么你应该得出结论宇宙是多核的吗?
@PeterK:完全正确 :) 这是物理学家 Brian Greene 对这张时间图的非常漂亮的可视化:youtube.com/watch?v=4BjGWLJNPcA&t=22m12s 这是第 22 分钟和第 12 秒的“时间幻觉 [完整纪录片]”。
是我还是他从一维内存模型(水平轴)切换到二维内存模型(同时平面)。我觉得这有点令人困惑,但也许那是因为我不是母语人士......仍然是一本非常有趣的书。
我活着看到相对论被用作一个简化的类比......
e
eran

这是一个多年前的问题,但非常受欢迎,值得一提的是学习 C++11 内存模型的绝佳资源。我认为总结他的演讲以使这成为另一个完整的答案没有意义,但鉴于这是实际编写标准的人,我认为值得观看演讲。

Herb Sutter 就 C++11 内存模型进行了长达三个小时的演讲,题为“原子<>武器”,可在 Channel9 网站 YouTube - part 1part 2 上找到。演讲非常技术性,涵盖以下主题:

优化、竞争和内存模型排序 – 内容:获取和释放排序 – 方式:互斥体、原子和/或栅栏 编译器和硬件代码生成和性能的其他限制:x86/x64、IA64、POWER、ARM 宽松原子

该演讲没有详细说明 API,而是详细说明了推理、背景、幕后和幕后(您是否知道将宽松语义添加到标准中只是因为 POWER 和 ARM 不能有效地支持同步加载?)。


@eran 你们碰巧有幻灯片吗?第 9 频道讨论页上的链接无效。
@athos 我没有,抱歉。尝试联系第 9 频道,我不认为删除是故意的(我的猜测是他们从 Herb Sutter 那里获得了链接,按原样发布,他后来删除了文件;但这只是一个猜测......)。
第 9 频道已停播,可用来源是 YouTube:part1part2。幻灯片可以下载here
谢谢@o_oTurtle!修复了答案中的链接。每当您遇到它们时,请随时自行进行此类修复。
P
Peter Mortensen

这意味着该标准现在定义了多线程,它定义了在多线程的上下文中发生的事情。当然,人们使用了不同的实现,但这就像问我们为什么应该有一个 std::string,而我们都可以使用一个自制的 string 类。

当您谈论 POSIX 线程或 Windows 线程时,这有点像您在谈论 x86 线程时的错觉,因为它是并发运行的硬件功能。 C++0x 内存模型可以保证,无论您使用的是 x86、ARM、MIPS,还是您能想到的任何其他东西。


Posix 线程不限于 x86。实际上,它们实施的第一个系统可能不是 x86 系统。 Posix 线程是独立于系统的,并且在所有 Posix 平台上都有效。它也不是真正的硬件属性,因为 Posix 线程也可以通过协作多任务处理来实现。但当然,大多数线程问题只出现在硬件线程实现上(有些甚至只出现在多处理器/多核系统上)。
P
Peter Mortensen

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

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

有趣的是,用于 C++ 的 Microsoft 编译器已经为 volatile 获取/释放语义,这是一个 C++ 扩展,用于处理 C++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx 中缺少内存模型的问题。但是,鉴于 Windows 仅在 x86 / x64 上运行,这并不能说明什么(英特尔和 AMD 内存模型可以轻松高效地在语言中实现获取/释放语义)。


确实,在编写答案时,Windows 仅在 x86/x64 上运行,但 Windows 有时在 IA64、MIPS、Alpha AXP64、PowerPC 和 ARM 上运行。今天,它可以在各种版本的 ARM 上运行,这与 x86 在内存方面完全不同,而且在任何地方都没有那么宽容。
该链接有些损坏(说“Visual Studio 2005 Retired documentation”)。关心更新吗?
即使写了答案也不是真的。
“同时访问相同的内存”以冲突的方式访问
n
ninjalj

如果您使用互斥锁来保护您的所有数据,您真的不必担心。互斥锁总是提供足够的顺序和可见性保证。

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

以前,原子操作将使用编译器内在函数或一些更高级别的库来完成。栅栏将使用特定于 CPU 的指令(内存屏障)来完成。


之前的问题是不存在互斥锁(根据 C++ 标准)。因此,为您提供的唯一保证是互斥体制造商,只要您不移植代码就可以了(因为很难发现对保证的细微更改)。现在我们得到了标准提供的保证,应该在平台之间移植。
@Martin:无论如何,一件事是内存模型,另一件事是在该内存模型之上运行的原子和线程原语。
另外,我的观点主要是,以前几乎没有语言级别的内存模型,它恰好是底层 CPU 的内存模型。现在有一个内存模型,它是核心语言的一部分; OTOH、互斥锁等总是可以作为一个库来完成。
对于试图编写互斥体库的人来说,这也可能是一个真正的问题。当 CPU、内存控制器、内核、编译器和“C 库”都由不同的团队实现时,其中一些人对于这些东西应该如何工作存在激烈的分歧,嗯,有时这些东西我们系统程序员必须为应用程序级别呈现一个漂亮的外观,这根本不愉快。
不幸的是,如果您的语言中没有一致的内存模型,那么用简单的互斥锁来保护您的数据结构是不够的。有各种编译器优化在单线程上下文中是有意义的,但是当多个线程和 cpu 内核发挥作用时,内存访问的重新排序和其他优化可能会产生未定义的行为。有关详细信息,请参阅 Hans Boehm 的“线程不能作为库实现”:citeseer.ist.psu.edu/viewdoc/…
M
Mike Spear

上述答案涉及 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<>。如果它们不是原子的,那么它们可以在被写入(在 writer() 中)的同时被读取(在 reader() 中)。根据 C++ 内存模型,这是一场竞赛即使 reader() 从未真正使用过数据。此外,如果它们不是原子的,那么编译器可以将每个值的第一次读取缓存在寄存器中。显然您不希望这样...您希望在 reader() 中的 while 循环的每次迭代中重新读取。

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

论文给出了一些解决方案。今天性能最好的可能是在第二次读取 seqlock 之前使用 atomic_thread_fencememory_order_relaxed 的那个。在论文中,它是图 6。我不是在这里复制代码,因为读过这里的任何人都应该阅读这篇论文。它比这篇文章更精确和完整。

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

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


c
curiousguy

C 和 C++ 曾经由格式良好的程序的执行跟踪定义。

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

这意味着这些语言定义根本没有任何意义,因为没有逻辑方法可以混合这两种方法。特别是,对互斥体或原子变量的破坏没有很好的定义。


我同意您对改进语言设计的强烈愿望,但我认为如果您的答案以一个简单的案例为中心,您的回答会更有价值,您清楚而明确地展示了该行为如何违反特定的语言设计原则。在那之后,如果你允许我,我强烈建议你在这个答案中给出一个很好的论据来证明这些观点的相关性,因为它们将与 C++ 设计所感知的巨大生产力优势的相关性形成对比
@MatiasHaeussler 我认为您误读了我的回答;我不反对这里对特定 C++ 特性的定义(我也有很多这样的尖锐批评,但不是在这里)。 我在这里争辩说,在 C++(或 C)中没有明确定义的构造。 整个 MT 语义完全是一团糟,因为您不再有顺序语义。 (我相信 Java MT 是坏的,但更少。)“简单的例子”几乎是任何 MT 程序。如果您不同意,欢迎您回答我关于 how to prove correctness of MT C++ programs 的问题。
有趣的是,我想我在阅读您的问题后更了解您的意思。如果我是对的,您指的是不可能为 C++ MT 程序的正确性开发证明。在这种情况下,我会说,对我来说,这对计算机编程的未来非常重要,特别是对于人工智能的到来。但我也要指出,对于绝大多数在堆栈溢出中提出问题的人来说,他们甚至没有意识到这一点,即使在理解了你的意思并变得感兴趣之后
“关于计算机程序的可演示性的问题是否应该发布在 stackoverflow 或 stackexchange 中(如果两者都没有,在哪里)?”这个似乎是元stackoverflow的一个,不是吗?
@MatiasHaeussler 1)C 和 C++ 本质上共享原子变量、互斥体和多线程的“内存模型”。 2)与此相关的是拥有“记忆模型”的好处。我认为收益为零,因为模型不健全。