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;计算机基础等
评论 (0)
  • 你是不是也有在公共场合被偷看手机或笔电的经验呢?科技时代下,不少现代人的各式机密数据都在手机、平板或是笔电等可携式的3C产品上处理,若是经常性地需要在公共场合使用,不管是工作上的机密文件,或是重要的个人信息等,民众都有防窃防盗意识,为了避免他人窥探内容,都会选择使用「防窥保护贴片」,以防止数据外泄。现今市面上「防窥保护贴」、「防窥片」、「屏幕防窥膜」等产品就是这种目的下产物 (以下简称防窥片)!防窥片功能与常见问题解析首先,防窥片最主要的功能就是用来防止他人窥视屏幕上的隐私信息,它是利用百叶窗的
    百佳泰测试实验室 2025-04-30 13:28 245浏览
  • 4月22日下午,备受瞩目的飞凌嵌入式「2025嵌入式及边缘AI技术论坛」在深圳深铁皇冠假日酒店盛大举行,此次活动邀请到了200余位嵌入式技术领域的技术专家、企业代表和工程师用户,共享嵌入式及边缘AI技术的盛宴!1、精彩纷呈的展区产品及方案展区是本场活动的第一场重头戏,从硬件产品到软件系统,从企业级应用到高校教学应用,都吸引了现场来宾的驻足观看和交流讨论。全产品矩阵展区展示了飞凌嵌入式丰富的产品线,从嵌入式板卡到工控机,从进口芯片平台到全国产平台,无不体现出飞凌嵌入式在嵌入式主控设备研发设计方面的
    飞凌嵌入式 2025-04-28 14:43 146浏览
  • 一、gao效冷却与控温机制‌1、‌冷媒流动设计‌采用低压液氮(或液氦)通过毛细管路导入蒸发器,蒸汽喷射至样品腔实现快速冷却,冷却效率高(室温至80K约20分钟,至4.2K约30分钟)。通过控温仪动态调节蒸发器加热功率,结合温度传感器(如PT100铂电阻或Cernox磁场不敏感传感器),实现±0.01K的高精度温度稳定性。2、‌宽温区覆盖与扩展性‌标准温区为80K-325K,通过降压选件可将下限延伸至65K(液氮模式)或4K(液氦模式)。可选配475K高温模块,满足材料在ji端温度下的性能测试需求
    锦正茂科技 2025-04-30 13:08 185浏览
  • 在CAN总线分析软件领域,当CANoe不再是唯一选择时,虹科PCAN-Explorer 6软件成为了一个有竞争力的解决方案。在现代工业控制和汽车领域,CAN总线分析软件的重要性不言而喻。随着技术的进步和市场需求的多样化,单一的解决方案已无法满足所有用户的需求。正是在这样的背景下,虹科PCAN-Explorer 6软件以其独特的模块化设计和灵活的功能扩展,为CAN总线分析领域带来了新的选择和可能性。本文将深入探讨虹科PCAN-Explorer 6软件如何以其创新的模块化插件策略,提供定制化的功能选
    虹科汽车智能互联 2025-04-28 16:00 176浏览
  • 贞光科技代理品牌紫光国芯的车规级LPDDR4内存正成为智能驾驶舱的核心选择。在汽车电子国产化浪潮中,其产品以宽温域稳定工作能力、优异电磁兼容性和超长使用寿命赢得市场认可。紫光国芯不仅确保供应链安全可控,还提供专业本地技术支持。面向未来,紫光国芯正研发LPDDR5车规级产品,将以更高带宽、更低功耗支持汽车智能化发展。随着智能网联汽车的迅猛发展,智能驾驶舱作为人机交互的核心载体,对处理器和存储器的性能与可靠性提出了更高要求。在汽车电子国产化浪潮中,贞光科技代理品牌紫光国芯的车规级LPDDR4内存凭借
    贞光科技 2025-04-28 16:52 234浏览
  • 文/Leon编辑/cc孙聪颖‍2023年,厨电行业在相对平稳的市场环境中迎来温和复苏,看似为行业增长积蓄势能。带着对市场向好的预期,2024 年初,老板电器副董事长兼总经理任富佳为企业定下双位数增长目标。然而现实与预期相悖,过去一年,这家老牌厨电企业不仅未能达成业绩目标,曾提出的“三年再造一个老板电器”愿景,也因市场下行压力面临落空风险。作为“企二代”管理者,任富佳在掌舵企业穿越市场周期的过程中,正面临着前所未有的挑战。4月29日,老板电器(002508.SZ)发布了2024年年度报告及2025
    华尔街科技眼 2025-04-30 12:40 186浏览
  • 在智能硬件设备趋向微型化的背景下,语音芯片方案厂商针对小体积设备开发了多款超小型语音芯片方案,其中WTV系列和WT2003H系列凭借其QFN封装设计、高性能与高集成度,成为微型设备语音方案的理想选择。以下从封装特性、功能优势及典型应用场景三个方面进行详细介绍。一、超小体积封装:QFN技术的核心优势WTV系列与WT2003H系列均提供QFN封装(如QFN32,尺寸为4×4mm),这种封装形式具有以下特点:体积紧凑:QFN封装通过减少引脚间距和优化内部结构,显著缩小芯片体积,适用于智能门铃、穿戴设备
    广州唯创电子 2025-04-30 09:02 205浏览
  • 随着电子元器件的快速发展,导致各种常见的贴片电阻元器件也越来越小,给我们分辨也就变得越来越难,下面就由smt贴片加工厂_安徽英特丽就来告诉大家如何分辨的SMT贴片元器件。先来看看贴片电感和贴片电容的区分:(1)看颜色(黑色)——一般黑色都是贴片电感。贴片电容只有勇于精密设备中的贴片钽电容才是黑色的,其他普通贴片电容基本都不是黑色的。(2)看型号标码——贴片电感以L开头,贴片电容以C开头。从外形是圆形初步判断应为电感,测量两端电阻为零点几欧,则为电感。(3)检测——贴片电感一般阻值小,更没有“充放
    贴片加工小安 2025-04-29 14:59 198浏览
  • 网约车,真的“饱和”了?近日,网约车市场的 “饱和” 话题再度引发热议。多地陆续发布网约车风险预警,提醒从业者谨慎入局,这背后究竟隐藏着怎样的市场现状呢?从数据来看,网约车市场的“过剩”现象已愈发明显。以东莞为例,截至2024年12月底,全市网约车数量超过5.77万辆,考取网约车驾驶员证的人数更是超过13.48万人。随着司机数量的不断攀升,订单量却未能同步增长,导致单车日均接单量和营收双双下降。2024年下半年,东莞网约出租车单车日均订单量约10.5单,而单车日均营收也不容乐
    用户1742991715177 2025-04-29 18:28 204浏览
  • 浪潮之上:智能时代的觉醒    近日参加了一场课题的答辩,这是医疗人工智能揭榜挂帅的国家项目的地区考场,参与者众多,围绕着医疗健康的主题,八仙过海各显神通,百花齐放。   中国大地正在发生着激动人心的场景:深圳前海深港人工智能算力中心高速运转的液冷服务器,武汉马路上自动驾驶出租车穿行的智慧道路,机器人参与北京的马拉松竞赛。从中央到地方,人工智能相关政策和消息如雨后春笋般不断出台,数字中国的建设图景正在智能浪潮中徐徐展开,战略布局如同围棋
    广州铁金刚 2025-04-30 15:24 169浏览
  • 文/郭楚妤编辑/cc孙聪颖‍越来越多的企业开始蚕食动力电池市场,行业“去宁王化”态势逐渐明显。随着这种趋势的加强,打开新的市场对于宁德时代而言至关重要。“我们不希望被定义为电池的制造者,而是希望把自己称作新能源产业的开拓者。”4月21日,在宁德时代举行的“超级科技日”发布会上,宁德时代掌门人曾毓群如是说。随着宁德时代核心新品骁遥双核电池的发布,其搭载的“电电增程”技术也走进业界视野。除此之外,经过近3年试水,宁德时代在换电业务上重资加码。曾毓群认为换电是一个重资产、高投入、长周期的产业,涉及的利
    华尔街科技眼 2025-04-28 21:55 147浏览
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦