What is the Memory Model in C++11

C语言与CPP编程 2020-12-24 00:00

C++11其实主要就四方面内容,第一个是可变参数模板,第二个是右值引用,第三个是智能指针,第四个是内存模型(Memory Model)。

相对来说,这也是较难理解的几个特性,分别针对于泛型编程,内存优化,内存管理和并发编程。

并发编程是个非常大的模块,而在诸多内容底下有一个基本的概念,就是并发内存模型(Memory Model)。

那么,什么是内存模型?

1
Memory Model

早在之前介绍并发编程的文章中,我们就知道同步共享数据很重要。而同步可分为两种方式:原子操作和顺序约束。

原子操作是数据操作的最小单元,天生不可再分;顺序约束可以协调各个线程之间数据访问的先后顺序,避免数据竞争。

通常的同步方式会有两个问题,一是效率不够,二是死锁问题。导致效率不够是因为这些方式都是lock-based的。

当然,若非非常在意效率,完全可以使用这些同步方式,因其简单方便且不易出错。

若要追求更高的效率,需要学习lock-free(无锁)的同步方式。

内存模型,简单地说,是一种介于开发者和系统之间的并发约定,可以无锁地保证程序的执行逻辑与预期一致。

这里的系统包括编译器、处理器和缓存,各部分都想在自己的领域对程序进行优化,以提高性能,而这些优化会打乱源码中的执行顺序。尤其是在多线程上,这些优化会对共享数据造成巨大影响,导致程序的执行结果往往不遂人意。

内存模型,就是来解决这些优化所带来的问题。主要包含三个方面:

  • Atomic operations(原子操作)

  • Partial ordering of operations(局部执行顺序)

  • Visible effects of operations(操作可见性)

原子操作和局部执行顺序如前所述,「操作可见性」指的是不同线程之间操作共享变量是可见的。

原子数据的同步是由编译器来保证的,而非原子数据需要我们自己来规划顺序。

2

关系定义

这里有三种关系术语,

  • sequenced-before

  • happens-before

  • synchronizes-with

同一线程语句之间,若A操作在B操作之前执行,则表示为A sequenced-before B,A的执行结果对B可见。
而在不同线程的语句之间,若A操作在B操作之前就已发生,则表示为A happens-before B。该关系具有可传递性,也就是说,若A happens-before B,B happens-before C,则一定能得出A happens-before C。
若A操作的状态改变引发了B操作的执行,则表示为A synchronizes-with B。比如我们学过的事件、条件变量、信号量等等都会因一个条件(状态)满足,而执行相应的操作,这种状态关系就叫做synchronizes-with。
由于synchronizes-with的特性,可以借其实现happens-before关系。
内存模型就是提供一个操作的约束语义,借其可以满足上述关系,实现了顺序约束。

3

Atomics(原子操作)

原子操作的知识之前也介绍过,限于篇幅,便不再捉细节。
先来整体看一下原子操作支持的操作类型,后面再来讲应用。
这里挑两个来介绍一下相关操作,算是回顾。
第一个来讲atomic_flag,这是最简单的原子类型,代表一个布尔标志,可用它实现一个自旋锁:

1#include <atomic>
2#include <thread>
3#include <iostream>
4
5class spin_lock
6{

7    std::atomic_flag flag = ATOMIC_FLAG_INIT;
8public:
9    void lock() while(flag.test_and_set()); }
10
11    void unlock() { flag.clear(); }
12};
13
14spin_lock spin;
15int g_num = 0;
16void work()
17
{
18    spin.lock();
19
20    g_num++;
21
22    spin.unlock();
23}
24
25int main()
26
{
27    std::thread t1(work);
28    std::thread t2(work);
29    t1.join();
30    t2.join();
31
32    std::cout << g_num;
33
34    return 0;
35}

atomic_flag必须使用ATOMIC_FLAG_INIT初始化,该值就是0,也就是false。
只能通过两个接口来操作atomic_flag:
  • clear:清除操作,将值设为false。

  • test_and_set:将值设为true并返回之前的值。


第9行的lock()函数实现了自旋锁,当第一个线程进来的时候,由于atomic_flag为false,所以会通过test_and_set设置为true并返回false,第一个线程于是可以接着执行下面的逻辑。

当第二个线程进来时,flag为true,因此会一直循环,只有第一个线程中unlock了才会接着执行。由此保证了共享变量g_num。
第二个来讲atomic<bool>,它所支持的原子操作要比atomic_flag多。
一个简单的同步操作:

1#include <atomic>
2#include <thread>
3#include <iostream>
4#include <vector>
5#include <algorithm>
6#include <iterator>
7
8std::atomic<bool> flag{false};
9std::vector<int> shared_values;
10void work()
11
{
12    std::cout << "waiting" << std::endl;
13    while(!flag.load())
14    {
15        std::this_thread::sleep_for(std::chrono::milliseconds(5));
16    }
17
18    shared_values[1] = 2;
19    std::cout << "end of the work" << std::endl;
20}
21
22void set_value()
23
{
24    shared_values = { 789 };
25    flag = true;
26    std::cout << "data prepared" << std::endl;
27}
28
29int main()
30
{
31    std::thread t1(work);
32    std::thread t2(set_value);
33    t1.join();
34    t2.join();
35
36    std::copy(shared_values.begin(), shared_values.end(), std::ostream_iterator<int>(std::cout" "));
37
38    return 0;
39}

这里有两个线程,它们之间拥有执行顺序,只有先在set_value函数中设置好共享值,才能在work函数中修改。
通过flag的load函数可以获取原子值,在值未设置完成时其为false,所以会一直等待数据到来。当flag变为true时,表示数据已经设置完成,于是会继续工作。

4

Memory ordering(内存顺序)

是什么保证了上述原子操作能够在多线程环境下同步执行呢?

其实在所有的原子操作函数中都有一个可选参数memory_order。比如atomic<bool>的load()和store()原型如下:

bool std::_Atomic_bool::load(std::memory_order _Order = std::memory_order_seq_cst) const noexcept
void std::_Atomic_bool::store(bool _Value, std::memory_order _Order = std::memory_order_seq_cst) noexcept

这里的可选参数默认为memory_order_seq_cst,所有的memory_order可选值为:

enum memory_order {

    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
};

这就是C++提供的如何实现顺序约束的方式,通过指定特定的memory_order,可以实现前面提及的sequence-before、happens-before、synchronizes-with关系。

顺序约束是我们和系统之间的一个约定,约定强度由强到弱可以分为三个层次:

  • Sequential consistency(顺序一致性): memory_order_seq_cst
  • Acquire-release(获取与释放): memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel
  • Relaxed(松散模型): memory_order_relaxed

Sequential consistency保证所有操作在线程之间都有一个全局的顺序,Acquire-release保证在不同线程间对于相同的原子变量的写和读的操作顺序,Relaxed仅保证原子的修改顺序。

为何要分层次呢?

其实顺序约束和系统优化之间是一种零和博弈,约束越强,系统所能够做的优化便越少。

因此每个层次拥有效率差异,层次越低,优化越多,效率也越高,不过掌握难度也越大。

所有的Memory order按照操作类型,又可分为三类:

  • Read(读):memory_order_acquire,memory_order_consume

  • Write(写):memory_order_release

  • Read-modify-Write(读-改-写):memory_order_acq_rel,memory_order_seq_cst

Relaxed未定义同步和顺序约束,所以要单独而论。

例如load()就是Read操作,store()就是Write()操作,compare_exchange_strong就是Read-modify-Write操作。

这意味着你不能将一个Read操作的顺序约束,写到store()上。例如,若将memory_order_acquire写到store()上,不会产生任何效果。

我们先来从默认的Sequential consistency开始,往往无需设置,便默认是memory_order_seq_cst,可以写一个简单的生产者-消费者函数:

1std::string sc_value;
2std::atomic<bool> ready{false};
3
4void consumer()
5
{
6    while(!ready.load()) {}
7
8    std::cout << sc_value << std::endl;
9}
10
11void producer()
12
{
13    sc_value = "produce values";
14    ready = true;
15}
16
17int main()
18
{
19    std::thread t1(consumer);
20    std::thread t2(producer);
21    t1.join();
22    t2.join();
23
24    return 0;
25}

此时,执行顺序具有强保证性,一定是先执行了producer再执行的consumer。
用标准的关系术语来说就是,第13行的操作和第14行的操作是sequenced-before关系,第14行和第6行的操作是synchronizes-with关系,进而保证了14行的赋值操作一定在第6行的load()操作之前执行,也就是保证了happens-before关系。
Acquire-release就开始变得有些复杂,我们先以一个最简单的例子来看。

1class spin_lock
2{

3    std::atomic_flag flag = ATOMIC_FLAG_INIT;
4public:
5    spin_lock() {}
6
7    void lock() while(flag.test_and_set(std::memory_order_acquire)); }
8
9    void unlock() { flag.clear(std::memory_order_release); }
10};
11
12spin_lock spin;
13void work()
14
{
15    spin.lock();
16    // do something
17    spin.unlock();
18}
19
20int main()
21
{
22    std::thread t1(work);
23    std::thread t2(work);
24    t1.join();
25    t2.join();
26
27    return 0;
28}

clear()中使用了release,test_and_set()中使用了acquire,acquire和release操作之间是synchronizes-with的关系。

它的行为和之前使用sequential consistency默认参数的自旋锁一样,不过要更加轻便高效。

test_and_set()操作其实是个Read-modify-Write操作,不过依旧可以使用acquire操作。release禁止了所有在它之前或之后的写操作乱序,acquire禁止了所有在它之前或之后的读操作乱序。

在两个不同的线程之间,共同访问同一个原子是flag,所添加的顺序约束就是为了保证flag的修改顺序。

我们再来看一个更清晰的例子:

1std::atomic<bool> x{false}, y{false};
2std::atomic<int> z{0};
3void write()
4
{
5    // relaxed只保证修改顺序
6    x.store(truestd::memory_order_relaxed);
7
8    // release保证在它之前的所有写操作顺序一致
9    y.store(truestd::memory_order_release);
10}
11
12void read()
13
{
14    // acquire保证在它之前和之后的读操作顺序一致
15    while(!y.load(std::memory_order_acquire));
16
17    // relaxed只保证修改顺序
18    if(x.load(std::memory_order_relaxed))
19        ++z;
20}
21
22int main()
23
{
24    std::thread t1(write);
25    std::thread t2(read);
26    t1.join();
27    t2.join();
28
29    assert(z.load() != 0);
30
31    return 0;
32}

注意这是使用了relaxed、release和acquire三种约束。
relaxed只保证修改顺序,所以对于write()函数来说,一定是先执行x后执行y操作。不过若是将y也使用relaxed,虽然在write()中是先x后y的顺序,而在read()的眼中,可能是先y后x的顺序,这是优化导致的。
而因为y的读和写使用了acquire和release约束,所以可以保证在不同线程间对于相同的原子变量读和写的操作顺序一致。
同时,Acquire-release操作还拥有传递性,是典型的happens-before关系。
还是提供一个例子:

1std::vector<int> shared_value;
2std::atomic<bool> produced{false};
3std::atomic<bool> consumed{false};
4
5void producer()
6
{
7    shared_value = { 789 };
8
9    // A. realse happens-before B
10    produced.store(truestd::memory_order_release);
11}
12
13void delivery()
14
{
15    // B. acquire,A synchronizes with B
16    while(!produced.load(std::memory_order_acquire));
17
18    // B. release happens-beforeC
19    consumed.store(truestd::memory_order_release);
20}
21
22void consumer()
23
{
24    // C. acquire, B synchronizes with C
25    // therefore, A happens before C
26    while(!consumed.load(std::memory_order_acquire));
27
28    shared_value[1] = 2;
29}
30
31int main()
32
{
33    std::thread t1(consumer);
34    std::thread t2(producer);
35    std::thread t3(delivery);
36    t1.join();
37    t2.join();
38    t3.join();
39
40    std::copy(shared_value.begin(), shared_value.end(), std::ostream_iterator<int>(std::cout" "));
41
42    return 0;
43}

注释已经足够说明其中所以,便不细述。

5

Fences(栅栏)

看回先前的一个例子:

1std::atomic<bool> x{false}, y{false};
2std::atomic<int> z{0};
3void write()
4
{
5    // relaxed只保证修改顺序
6    x.store(truestd::memory_order_relaxed);
7    y.store(truestd::memory_order_relaxed);
8}
9
10void read()
11
{
12    // relaxed只保证修改顺序
13    while(!y.load(std::memory_order_relaxed));
14    if(x.load(std::memory_order_relaxed))
15        ++z;
16}


relaxed是最弱的内存模型,此处全使用relaxed,顺序将不再有保证。

也许在read()中看到的write()操作是先y后x,那么此时read()里面的if操作便无法满足,也就是说,++z不会被执行。

解决方法是结合fences来使用,只需添加两行代码:

1std::atomic<bool> x{false}, y{false};
2std::atomic<int> z{0};
3void write()
4
{
5    // relaxed只保证修改顺序
6    x.store(truestd::memory_order_relaxed);
7
8    std::atomic_thread_fence(std::memory_order_release);
9
10    y.store(truestd::memory_order_relaxed);
11}
12
13void read()
14
{
15    // relaxed只保证修改顺序
16    while(!y.load(std::memory_order_relaxed));
17
18    std::atomic_thread_fence(std::memory_order_acquire);
19
20    if(x.load(std::memory_order_relaxed))
21        ++z;
22}

fences位于relaxed操作之间,它像一个栅栏一样,可以保证前后的操作不会乱序。
具体细节,接着来看。
C++提供了两个类型的fences,
  • std::atomic_thread_fence:同步线程之间的内存访问。

  • std::atomic_signal_fence:同步同一线程上的signal handler和code running。

我们主要学习第一个线程的fence,它会阻止特定的操作穿过栅栏,约束执行顺序。

有三种类型的fences,

  • Full fence:阻止两个任意操作乱序。memory_order_seq_cst或memory_order_acq_rel。

  • Acquire fence:阻止读操作乱序,memory_order_acquire。

  • Release fence:阻止写操作乱序,memory_order_release。

用图来表示为:

图中间灰色的一杠就表示fence,红色表示禁止乱序,可以看到,除了Store-Load,其它操作都可以保障执行顺序。
同样也有效率差异,可以针对具体的操作来选择合适的fence。

6

总结

本篇内容挺复杂的,其实就包含三个方面:Atomic operations(原子操作)、Partial ordering of operations(局部执行顺序)和Visible effects of operations(操作可见性)。

面对一个复杂的概念,往往需要变换尺度来进行理解,若一开始便陷入诸多细节中去,难免迷失其中,看不到整体的结构。
所以这里其实也就是以我自己的理解来写,细节涉及不多,但整体结构已算完整,想了解更多具体细节可以参考C++ Concurrency in Action

C语言与CPP编程 C语言/C++开发,C语言/C++基础知识,C语言/C++学习路线,C语言/C++进阶,数据结构;算法;python;计算机基础等
评论
  • 国产光耦合器正以其创新性和多样性引领行业发展。凭借强大的研发能力,国内制造商推出了适应汽车、电信等领域独特需求的专业化光耦合器,为各行业的技术进步提供了重要支持。本文将重点探讨国产光耦合器的技术创新与产品多样性,以及它们在推动产业升级中的重要作用。国产光耦合器创新的作用满足现代需求的创新模式新设计正在满足不断变化的市场需求。例如,高速光耦合器满足了电信和数据处理系统中快速信号传输的需求。同时,栅极驱动光耦合器支持电动汽车(EV)和工业电机驱动器等大功率应用中的精确高效控制。先进材料和设计将碳化硅
    克里雅半导体科技 2024-11-29 16:18 168浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 41浏览
  • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
    电子与消费 2024-11-30 22:03 86浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 66浏览
  • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
    youyeye 2024-11-30 14:30 70浏览
  • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
    wuyu2009 2024-11-30 20:30 106浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 65浏览
  • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
    艾迈斯欧司朗 2024-11-29 16:55 167浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 102浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 83浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 37浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 98浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 51浏览
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 70浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦