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

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

| C ++ 11引入了标准化的内存模型,但这到底是什么意思?它将如何影响C ++编程? 这篇文章(由加文·克拉克(Gavin Clarke)引用赫伯·萨特)表示:   内存模型意味着C ++代码   现在有一个标准库可供调用   不管谁编译   以及它在哪个平台上运行。   有一种标准方法可以控制   不同的线程交谈   处理器的内存。      \“当您谈论拆分时   跨不同核心的[代码]   在标准中,我们正在谈论   内存模型。我们准备去   优化它而不破坏   以下假设人们会去   编写代码,”萨特说。 好吧,我可以记住这一段以及网上可以找到的类似段落(因为我自出生以来就有自己的记忆模型:P),甚至可以发布它作为对其他人提出的问题的答案,但是说实话,我没有完全明白这一点。 C ++程序员甚至以前都曾开发过多线程应用程序,那么,这是POSIX线程,Windows线程还是C ++ 11线程又有什么关系呢?有什么好处?我想了解底层细节。 我也感觉到C ++ 11内存模型与C ++ 11多线程支持某种程度上相关,因为我经常将两者结合在一起。如果是,究竟是什么?为什么要联系它们? 由于我不知道多线程的内部原理以及内存模型的一般含义,请帮助我理解这些概念。 :-)     

解决方法

首先,您必须学会像语言律师一样思考。 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之前运行),)3ѭ(如果在线程1之后运行)或
0 17
(如果在线程1分配给x之后但在分配给y之前运行)。 它不能打印的是“ 5”,因为C ++ 11中原子加载/存储的默认模式是强制顺序一致性。这只是意味着所有加载和存储必须“好像”在您在每个线程中写入的顺序进行,而线程之间的操作可以交错,但是系统会喜欢。因此,原子的默认行为既提供了原子性,又提供了装载和存储的顺序。 现在,在现代CPU上,确保顺序一致性可能很昂贵。特别是,编译器很可能在每次访问之间发出完全成熟的内存屏障。但是,如果您的算法可以容忍乱序的加载和存储;即,如果它需要原子性但不需要排序;也就是说,如果它可以容忍此程序的输出为“ 5”,那么您可以这样编写:
           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,memory_order_release);   cout << y.load(memory_order_acquire) << \" \";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;
这将我们带回到有序的装载和存储状态,因此不再有可能输出“ 5”,但是这样做的开销却很小。 (在这个简单的示例中,结果与成熟的顺序一致性相同;在较大的程序中,结果则不是。) 当然,如果要查看的唯一输出是
0 0
37 17
,则只需在原始代码周围包裹一个互斥体即可。但是,如果您已经读了那么多书,我敢打赌,您已经知道它是如何工作的,并且这个答案已经比我打算的要长:-)。 因此,底线。互斥体很棒,并且C ++ 11对其进行了标准化。但有时出于性能原因,您需要较低级别的原语(例如,经典的双重检查锁定模式)。新标准提供了诸如互斥锁和条件变量之类的高级小工具,还提供了诸如原子类型和各种不同的内存屏障之类的低级小工具。因此,现在您可以完全使用标准指定的语言编写复杂的高性能并发例程,并且可以确定您的代码可以在当今和未来的系统上编译并保持不变。 坦率地说,除非您是专家并且致力于一些严肃的低级代码,否则您应该坚持使用互斥体和条件变量。这就是我打算做的。 有关这些内容的更多信息,请参见此博客文章。     ,我将提供一个类比,以了解内存一致性模型(或简称为内存模型)。它的灵感来自于莱斯利·兰波特(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的加载来完成此操作。当使用多个内核时,每个内核都有自己的旁路写缓冲区。没有写缓冲区,硬件是SC,但是没有写缓冲区,则硬件使SC在多核处理器中在体系结构上可见。      如果内核具有一个非FIFO写缓冲区,可以使存储以与输入顺序不同的顺序离开,则可能会发生存储-存储重排序。如果第一个存储在第二个命中时未命中高速缓存,或者第二个存储可以与较早的存储合并(即在第一个存储之前),则可能会发生这种情况。负载重排序也可能发生在动态调度的内核上,这些内核以非程序顺序执行指令。这可以与对另一个核心上的存储进行重新排序的行为相同(您能否提出一个在两个线程之间进行交织的示例?)。使用较晚的存储对较早的加载进行重新排序(对存储进行重新排序)可能会导致许多不正确的行为,例如在释放保护它的锁之后加载值(如果存储是解锁操作)。请注意,即使使用以程序顺序执行所有指令的内核,由于通常实现的FIFO写缓冲区中的本地旁路,也会导致存储装入重新排序。 因为有时会混淆高速缓存一致性和内存一致性,所以也有这样的引用是有启发性的:   与一致性不同,缓存一致性对于软件既不可见也不是必需的。 Coherence试图使共享内存系统的缓存在功能上与单核系统中的缓存一样不可见。正确的一致性可确保程序员无法通过分析加载和存储的结果来确定系统是否以及在何处具有缓存。这是因为正确的一致性可确保高速缓存永远不会启用新的或不同的功能行为(程序员可能仍能够使用时序信息来推断可能的高速缓存结构)。高速缓存一致性协议的主要目的是使每个存储位置的单写多读器(SWMR)保持不变。   一致性和一致性之间的重要区别是,一致性是在每个内存位置的基础上指定的,而一致性是在所有内存位置中指定的。 继续我们的思维图景,SWMR不变性对应于物理要求,即在任一位置最多存在一个粒子,但在任何位置都可以有无限数量的观察者。     ,这是一个已有多年历史的问题,但是它非常流行,值得一提的是一个很棒的资源,用于学习C ++ 11内存模型。为了得出另一个完整答案,我认为没有必要对他的发言进行总结,但是鉴于实际是编写标准的人,我认为值得一听。 Herb Sutter在Channel9网站(第1部分和第2部分)上进行了长达三个小时的名为“ atomic <> Weapons \”的C ++ 11内存模型的讨论。该讨论是非常技术性的,涉及以下主题: 优化,竞争和内存模型 订购–内容:获取并发布 订购–操作方式:互斥体,原子和/或栅栏 编译器和硬件的其他限制 代码生成和性能:x86 / x64,IA64,POWER,ARM 弛豫原子 讨论的内容不是在API上,而是在幕后和幕后的推理,背景(您是否知道宽松的语义只是因为POWER和ARM不有效支持同步负载而被添加到标准中?) 。     ,        这意味着该标准现在定义了多线程,并且定义了在多线程的上下文中发生的情况。当然,人们使用了不同的实现,但这就像在问为什么我们都可以使用本级的
string
类时,为什么我们应该有
std::string
。 当您谈论POSIX线程或Windows线程时,这实际上是您在谈论x86线程,这是一种错觉,因为它是同时运行的硬件功能。无论您使用的是x86,ARM,MIPS还是其他任何可用的东西,C ++ 0x内存模型都可以保证。     ,        对于未指定内存模型的语言,您正在为处理器架构指定的语言和内存模型编写代码。处理器可以选择重新排序内存访问以提高性能。因此,如果您的程序有数据争用(数据争用是指多个内核/超线程有可能同时访问同一内存),则由于它对处理器内存模型的依赖,因此您的程序不是跨平台的。您可以参考Intel或AMD软件手册,以了解处理器如何重新排序内存访问。 非常重要的是,锁(以及带有锁的并发语义)通常以跨平台的方式实现...因此,如果您在没有数据竞争的多线程程序中使用标准锁,则不必担心跨平台内存楷模。 有趣的是,用于C ++的Microsoft编译器已经获取/发布了volatile语义,这是C ++的扩展,可以解决C ++中缺少内存模型的问题http://msdn.microsoft.com/zh-cn/library/12a04hfd(v=vs .80).aspx。但是,考虑到Windows仅在x86 / x64上运行,这并不能说明太多(Intel和AMD内存模型使以一种语言实现获取/发布语义变得容易且高效)。     ,        如果您使用互斥锁来保护所有数据,则实际上不必担心。互斥锁始终提供足够的顺序和可见性保证。 现在,如果您使用原子或无锁算法,则需要考虑内存模型。内存模型精确描述了原子何时提供排序和可见性保证,并提供了用于手动编码保证的便携式围栏。 以前,原子将使用编译器内部函数或更高级别的库来完成。可以使用特定于CPU的指令(内存屏障)来完成防护。     ,上面的答案涉及C ++内存模型的最基本方面。实际上,至少在程序员过度优化之前(例如,通过尝试放松太多事情),大多数使用“ 14”就“正常工作”。 在一个地方,错误仍然很常见:序列锁。 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
}
起初接缝不直观,
data1
data2
必须为
atomic<>
。如果它们不是原子的,则可以在写入它们的同时(
writer()
)同时读取它们(
reader()
)。根据C ++内存模型,即使
reader()
从未实际使用数据,这也是一场竞赛。另外,如果它们不是原子的,则编译器可以将每个值的第一次读取缓存在寄存器中。显然,您不希望...要在
reader()
中的
while
循环的每次迭代中重新读取。 使它们成为
atomic<>
,并以
memory_order_relaxed
访问它们也是不够的。这样做的原因是seq的读取(在ѭ19中)仅具有获取语义。简单来说,如果X和Y是内存访问,X在Y之前,X不是获取或释放,并且Y是获取,则编译器可以在X之前对Y进行重新排序。如果Y是seq的第二次读取,并且X如果是读取数据,那么这种重新排序将破坏锁的实现。 本文给出了一些解决方案。今天性能最好的一个可能是在第二次读取seqlock之前使用
atomic_thread_fence
memory_order_relaxed
的那个。在本文中,它是图6。在这里,我不会在这里复制代码,因为到目前为止已经读过本文的人都应该阅读本文。它比这篇文章更加精确和完整。 最后一个问题是使29号变量成为原子级可能是不自然的。如果您无法编写代码,则需要非常小心,因为从非原子转换为原子仅对原始类型合法。 C ++ 20应该加上30,这将使此问题更容易解决。 总结一下:即使您认为自己了解C ++内存模型,在滚动自己的序列锁之前也应该非常小心。     ,        C和C ++过去是由格式良好的程序的执行跟踪定义的。 现在,它们一半是由程序的执行跟踪定义的,另一半是由对同步对象的许多排序的后验的。 这意味着这些语言定义根本没有意义,因为没有逻辑方法来混合这两种方法。特别是,互斥锁或原子变量的销毁定义不充分。     

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-