C++多线程内存模型

文中图片摘自《c++ concurrency in action》用于自我学习,存在不足,还请指正。

内存模型基础

在C/C++中,无论是常规的整型变量,还是结构体或是C++中的类,在计算机中都是在一定的内存位置进行存储的。而这个内存位置是在初始化就已经确定好,不同的内存位置,那么程序访问变量的顺序也有就不同。同样,在多线程程序中,如果程序的执行顺序不同,那么结果就有所区别。因此多线程程序中,原子操作是更接近于底层的操作,数据的存储顺序需要指定,以明确程序的运行顺序,达到避免数据竞争的作用。

常见模型介绍

在标准库<atomic>中,原子操作也提供了相关的参数进行设置数据存储顺序。原子操作可以分为三类操作,其可选择的顺序如下:
1.Store操作:memory_order_relaxed,memory_order_release,memory_order_seq_cst;
2.Load操作:memory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_seq_cst;
3.Read-modify-write(读-改-写)操作:memory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel,memory_order_seq_cst。
所有操作的默认顺序都是memory_order_seq_cst。
虽然有6个选择,但是只表示3种内存模型
1.排序一致序列(sequentially consistent)
2.获取释放序列(memory_order_consume、memory_order_acquire、memory_order_release、
memory_order_acq_rel)
3.自由序列(memory_oder_relaxed)

下面简单介绍三种内存模型

1.排序一致序列

程序中的行为从任意角度去看,序列顺序都保持一致。如果原子类型实例上的所有操作都是序列一致的,那么一个多线程程序的行为,就以某种特殊的排序执行,好像单线程那样,是可以预测执行顺序的。这是最容易理解的内存序列,所以设置为默认方式,但是简单存在一定性能上的代价。
如:对于多处理机计算机,因为整个序列的操作都需要在多个处理机上保持一致,这就需要对处理器间同步操作进行扩展,其代价是很大的,这降低了处理机的性能。

在这里插入图片描述
在这里插入图片描述
如上程序,assert()永远不会触发,同一变量的操作还是服从先发执行的关系,也就是存储store()操作发生在load()之前;在顺序一致序列中,各个线程按照一定顺序进行,线程之间自动进行了同步。例如:如果一个线程看到了x.load()为true,y.load()为false,那么可以推断出x.store()线程一定发生在y.store()线程之前,这就是这几个线程进行了同步,线程能够互相看到其他线程及时更新的数据。
如下图的实线表示的执行顺序
在这里插入图片描述
非排序一致序列内存模型,以memory_order_relaxed为例,并不能保证两个线程进行了同步。

2.自由序列

以自由序列进行原子操作时,线程之间没有任何的同步关系。通过例子进行讲述自由序列的顺序问题。
在这里插入图片描述
程序assert()可能会触发。在线程a中,对x和y进行了store()操作,但是线程b与a没有同步关系,所以在执行线程b时,执行y.load()操作可能会获得false,一直进行循环等待,并再次进行访问y.load();可能会获得线程a中对y.store()的更新得到true,然后跳出循环,执行x.load(),但是x与y并没有相关性,所以同样的可能会得到false的结果,就会触发assert()。这运行的结果充满了未知性、不确定性。
其中一种可能的执行顺序

在这里插入图片描述
也就是说memory_order_relaxed内存模型线程间是不存在任何同步关系。在多线程中,没有进行数据同步,会出现与设想的输出不一致,尽量不要单独使用这种模型,如果使用一定要添加其他原子操作或是内存删栏来保证数据的同步。

3.获取-释放序列

这个序列是自由序列的加强版;虽然没有统一的顺序,但是该模型引入了同步
其中原子加载就是获取操作(memory_order_acquire),原子存储就是释放操作(memory_order_release),原子读-改-写操作(如fetch_add()或是exchange())在这里,不是获取,就是释放,或是两者皆有的操作(memory_order_acq_rel)。内存模型中,释放操作和获取操作同步

程序见排序一致序列

程序的assert()可能会触发(如自由排序那样),因为x和y是由两个不同进程写入,所以序列中线程的每一次释放到获取都不会影响到其他的线程操作。
下图为执行的过程。
在这里插入图片描述
图中可以看出,如前面自由序列一样,线程之间看到的数据可能出现完全不同。因为特定的执行顺序,而且两线程之间也没有进行同步。
将程序进行更改为如下:
在这里插入图片描述
上述程序不会触发assert()。write_x()与write_y()合并为一个函数,对x和y在同一个进程中写入,在线程b中读取y.load()时,一直自旋等到y.load()返回true,那么读取x.load()时,因为先行关系(x.store()在y.store()之前),所以x.store()已经将true保存,而且x.store()与y.store()是同一个线程,也就是同一内存,若线程b能看到y.load()是true,跳出循环,那么必然能看到x.load()是true;这种情况不会触发assert()。加载与释放操作必须成对,无论有何影响,释放操作存储的值,必须要让获取操作看到,也就是加载和释放操作进行同步传递数据。

特别的:

在原子操作载入指向数据的指针时,可以使用memory_order_consume内存顺序作为加载语义,memory_order_release作为存储语义,可以进行同步,因为memory_order_consume是完全数据相关的,可以携带数据;
在这里插入图片描述
如上述程序,assert()的#4和#5不会触发,因为p.load标记为memory_order_consume,这就表示只有p.store()操作先行于p.load()操作,前面的那些a.store()操作不保证先行;而且使用memory_order_consume标记,也表示x变量对加载p操作有数据相关性,即能通过此获得x变量的数据,所以不会触发。但是a.store()、a.load()使用的自由序列,且与p没有数据相关性,所以assert()的#6会触发

多线程内存模型是指在多线程环境下,不同线程之间共享的内存模型。在多线程编程中,多个线程可以同时访问和修改同一个共享变量,但由于线程之间的并发执行,可能会出现一些并发问题,如数据竞争、原子性问题等,因此需要通过内存模型来规定多线程中共享变量的访问和修改规则,以保证线程之间的正确协作。 常用的多线程内存模型有两种:顺序一致性内存模型和Java内存模型(Java Memory Model,JMM)。 顺序一致性内存模型是指对于每个线程来说,该线程的所有操作都是按照程序的顺序执行的,且所有线程之间的操作是按照全局顺序来执行的。这种内存模型相对简单,易于理解,但对程序的执行速度有一定的限制。 Java内存模型是针对Java语言的多线程内存模型。Java内存模型是基于顺序一致性内存模型的,但相对于顺序一致性内存模型,Java内存模型允许一定程度上的重排序,以提高程序的执行效率。Java内存模型主要定义了共享变量的访问规则,如可见性、原子性等,并通过使用volatile关键字和synchronized关键字等机制来实现线程之间的同步与协作。 对于多线程内存模型的理解和正确使用,对于编写高效且正确的多线程程序至关重要。在编写多线程程序时,需要根据具体需要选择合适的内存模型,并遵循相应的编程规范和约定,以确保多线程程序的正确性和可靠性。此外,还可以利用锁、原子类、线程安全的数据结构等工具来保证多线程程序的正确性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值