当前位置: 首页 > news >正文

C++并发编程-11. C++ 原子操作和内存模型

简介

  • 本文介绍C++ 内存模型相关知识,包含几种常见的内存访问策略。

改动序列

在这里插入图片描述

  • 在一个C++程序中,每个对象都具有一个改动序列,它由所有线程在对象上的全部写操作构成,其中第一个写操作即为对象的初始化。大部分情况下,这个序列会随程序的多次运行而发生变化,但是在程序的任意一次运行过程中,所含的全部线程都必须形成相同的改动序列。

  • 改动序列基本要求如下

    • 1 只要某线程看到过某个对象,则该线程的后续读操作必须获得相对新近的值,并且,该线程就同一对象的后续写操作,必然出现在改动序列后方。
    • 2 如果某线程先向一个对象写数据,过后再读取它,那么必须读取前面写的值。
    • 3 若在改动序列中,上述读写操作之间还有别的写操作,则必须读取最后写的值。
    • 4 在程序内部,对于同一个对象,全部线程都必须就其形成相同的改动序列,并且在所有对象上都要求如此.
    • 5 多个对象上的改动序列只是相对关系,线程之间不必达成一致

原子类型

  • 标准原子类型的定义位于头文件内。我们可以通过atomic<>定义一些原子类型的变量,如atomic,atomic 这些类型的操作全是原子化的。
  • 从C++17开始,所有的原子类型都包含一个静态常量表达式成员变量,std::atomic::is_always_lock_free。这个成员变量的值表示在任意给定的目标硬件上,原子类型X是否始终以无锁结构形式实现。如果在所有支持该程序运行的硬件上,原子类型X都以无锁结构形式实现,那么这个成员变量的值就为true;否则为false。
  • 只有一个原子类型不提供is_lock_free()成员函数:std::atomic_flag 。类型std::atomic_flag的对象在初始化时清零,随后即可通过成员函数test_and_set()查值并设置成立,或者由clear()清零。整个过程只有这两个操作。其他的atomic<>的原子类型都可以基于其实现。
  • std::atomic_flag的test_and_set成员函数是一个原子操作,他会先检查std::atomic_flag当前的状态是否被设置过,
    • 1 如果没被设置过(比如初始状态或者清除后),将std::atomic_flag当前的状态设置为true,并返回false。
    • 2 如果被设置过则直接返回ture。
  • 对于std::atomic类型的原子变量,还支持load()和store()、exchange()、compare_exchange_weak()和compare_exchange_strong()等操作。

在这里插入图片描述
在这里插入图片描述

  • 需要注意的是,所有原子类型都不支持拷贝和赋值。因为该操作涉及了两个原子对象:要先从另外一个原子对象上读取值,然后再写入另外一个原子对象。而对于两个不同的原子对象上单一操作不可能是原子的。

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
免锁(Lock-Free):指原子操作不依赖传统的互斥锁(如 std::mutex),而是直接通过 CPU 的原子指令(如 x86 的 LOCK CMPXCHG)实现线程安全。功能:查询该原子类型的操作是否由硬件直接支持免锁。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

内存次序

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 对于原子类型上的每一种操作,我们都可以提供额外的参数,从枚举类std::memory_order取值,用于设定所需的内存次序语义(memory-ordering semantics)。

  • 枚举类std::memory_order具有6个可能的值,

  • 存储(store)操作,可选用的内存次序有
    std::memory_order_relaxed、std::memory_order_release或std::memory_order_seq_cst。

  • 载入(load)操作,可选用的内存次序有
    std::memory_order_relaxed、std::memory_order_consume、std::memory_order_acquire或std::memory_order_seq_cst。

  • “读-改-写”(read-modify-write)操作,可选用的内存次序有
    std::memory_order_relaxed、std::memory_order_consume、std::memory_order_acquire、std::memory_order_release、std::memory_order_acq_rel或std::memory_order_seq_cst

原子操作默认使用的是std::memory_order_seq_cst次序。

  • 这六种内存顺序相互组合可以实现三种顺序模型 (ordering model)

Sequencial consistent ordering. 实现同步, 且保证全局顺序一致 (single total order) 的模型. 是一致性最强的模型, 也是默认的顺序模型.
Acquire-release ordering. 实现同步, 但不保证保证全局顺序一致的模型.
Relaxed ordering. 不能实现同步, 只保证原子性的模型.

在这里插入图片描述

实现自旋锁

  • 自旋锁是一种在多线程环境下保护共享资源的同步机制。它的基本思想是,当一个线程尝试获取锁时,如果锁已经被其他线程持有,那么该线程就会不断地循环检查锁的状态,直到成功获取到锁为止。
  • 那我们用这个std:atomic_flag实现一个自旋锁。

#include <iostream>
#include <atomic>
#include <thread>// 自旋锁 begin
class SpinLock {
public:void lock() {//1 处while (flag.test_and_set(std::memory_order_acquire)); // 自旋等待,直到成功获取到锁}void unlock() {//2 处flag.clear(std::memory_order_release); // 释放锁}
private:std::atomic_flag flag = ATOMIC_FLAG_INIT;
};void TestSpinLock() {SpinLock spinlock;std::thread t1([&spinlock]() {spinlock.lock();for (int i = 0; i < 3; i++) {std::cout << "*";}std::cout << std::endl;spinlock.unlock();});std::thread t2([&spinlock]() {spinlock.lock();for (int i = 0; i < 3; i++) {std::cout << "?";}std::cout << std::endl;spinlock.unlock();});t1.join();t2.join();
}
// 自旋锁 end
int main()
{TestSpinLock();/****???*/return 0;
}

1 处 在多线程调用时,仅有一个线程在同一时刻进入test_and_set,因为atomic_flag初始状态为false,所以test_and_set将atomic_flag设置为true,并且返回false。

比如线程A调用了test_and_set返回false,这样lock函数返回,线程A继续执行加锁区域的逻辑。此时线程B调用test_and_set,test_and_set会返回true,导致线程B在while循环中循环等待,达到自旋检测标记的效果。当线程A直行至2处调用clear操作后,atomic_flag被设置为清空状态,线程B调用test_and_set会将状态设为成立并返回false,B线程执行加锁区域的逻辑。

我们看到在设置时使用memory_order_acquire内存次序,在清除时使用了memory_order_release内存次序。

宽松内存序

为了给大家介绍不同的字节序,我们先从最简单的字节序std::memory_order_relaxed(宽松字节序)介绍。
因为字节序是为了实现改动序列的,所以为了理解字节序还要结合改动序列讲起。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述在这里插入图片描述


// 宽松内存次序 begin
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{//write_x_then_y负责将x和y存储为true。x.store(true, std::memory_order_relaxed);  // 1y.store(true, std::memory_order_relaxed);  // 2
}
void read_y_then_x() 
{//read_y_then_x负责读取x和y的值。while (!y.load(std::memory_order_relaxed)) { // 3std::cout << "y load false" << std::endl;}if (x.load(std::memory_order_relaxed)) { //4++z;}
}
void TestOrderRelaxed() {std::thread t1(write_x_then_y);std::thread t2(read_y_then_x);t1.join();t2.join();assert(z.load() != 0); // 5
}
// 宽松内存次序  end
  • 上面的代码assert断言z不为0,但有时运行到5处z会等于0触发断言。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


void TestOderRelaxed2() {std::atomic<int> a{ 0 };std::vector<int> v3, v4;std::thread t1([&a]() {for (int i = 0; i < 10; i += 2) {a.store(i, std::memory_order_relaxed);}});std::thread t2([&a]() {for (int i = 1; i < 10; i += 2)a.store(i, std::memory_order_relaxed);});std::thread t3([&v3, &a]() {for (int i = 0; i < 10; ++i)v3.push_back(a.load(std::memory_order_relaxed));});std::thread t4([&v4, &a]() {for (int i = 0; i < 10; ++i)v4.push_back(a.load(std::memory_order_relaxed));});t1.join();t2.join();t3.join();t4.join();for (int i : v3) {std::cout << i << " ";}std::cout << std::endl;for (int i : v4) {std::cout << i << " ";}std::cout << std::endl;
}

在这里插入图片描述

术语总括

  • sequenced-before是一种单线程上的关系,这是一个非对称,可传递的成对关系。
  • happens-before关系是sequenced-before关系的扩展,因为它还包含了不同线程之间的关系。这是一个非对称,可传递的关系。(如果A happens-before B,则A的内存状态将在B操作执行之前就可见,这就为线程间的数据访问提供了保证。)
  • synchronizes-with描述的是一种状态传播(propagate)关系。如果A synchronizes-with B,则就是保证操作A的状态在操作B执行之前是可见的。

先行(Happens-before)

在这里插入图片描述

顺序先行(sequence-before)

在这里插入图片描述

线程间先行

在这里插入图片描述
在这里插入图片描述

依赖关系

在这里插入图片描述
在这里插入图片描述

Happens-before不代表质量执行顺序

在这里插入图片描述
在这里插入图片描述

脑图

在这里插入图片描述

总结

本文介绍了3种内存模型,包括全局一致性模型,同步模型以及最宽松的原子模型,以及6种内存序,下一篇将介绍如何利用6中内存序达到三种模型的效果。

memory_order_relaxed(最宽松的内存序)
1. 作用于原子性
线程1操作变量m,不会被线程B干扰
2. 不具有 synchronizes-with
线程1对变量m进行写了,线程2可能读到写之前的也可能读到写之后的
3.对于同一个原子变量,在同一个线程中具有happens-before关系, 在同一线程中不同的原子变量不具有happens-before关系,可以乱序执行。
乱序执行:2可能跑到1前面
atmoic m,b;
m.store(1,memory_order_relaxed); 1
b.store(2,memory_order_relaxed); 2

4. 多线程情况下不具有happens-before关系。
  1. A synchronizes-with B 同步 === A happens-before B A操作的结果对B可见

  2. A happens-before B 先行 A操作的结果对B可见(不是执行的顺序关系)

  3. 顺序先行(sequenced-before):a操作先行于b 具有传递性
    单线程先行
    多线程先行
    A synchronizes-with B == A sequenced-before B

  4. 依赖关系
    carries-dependency
    单线程:a “sequenced-before” b, 且 b 依赖 a 的数据, 则 a “carries a dependency into” b. 称作 a 将依赖关系带给 b, 也理解为b依赖于a。

    dependency-oredered before
    多线程情况:
    线程1执行操作A(比如对i自增),线程2执行操作B(比如根据i访问字符串下表的元素), 如果线程1先于线程2执行,且操作A的结果对操作B可见,我们将这种叫做
    A “dependency-ordered before” B.

http://www.dtcms.com/a/270954.html

相关文章:

  • Linux驱动学习day20(pinctrl子系统驱动大全)
  • Ubuntu防火墙缺失问题(unit firewalld.service could not be found, ubuntu 22)
  • EFK9.0.3 windows搭建
  • Linux系统管理实战:生成大文件与定位磁盘挂载点
  • 专题:2025母婴行业洞察报告|附60+份报告PDF汇总下载
  • Linux中shell(外壳)和内核(kernel)的关系
  • Claude Code:终端上的 AI 编码助手,潜力与挑战并存
  • 从零用java实现 小红书 springboot vue uniapp(13)模仿抖音视频切换
  • 华为数通HCIA vs HCIP:新手入门选哪个更合适?
  • 利用sCMOS科学相机测量激光散射强度
  • Rk3568驱动开发_阻塞IO_15
  • SQL Server通过存储过程实现飞书消息卡片推送
  • Live555-RTSP服务器
  • nl2sql的解药pipe syntax
  • 【工具变量】上市公司企业金融强监管数据、资管新规数据(2001-2024年)
  • 【YOLOv11-目标检测】目标检测数据格式(官方说明)
  • S7-200 SMART :通过以太网下载程序详细步骤
  • React、Vue、Angular的性能优化与源码解析概述
  • Qt6中模态与非模态对话框区别
  • 供应链管理-采购:谈判方式、理念、技巧
  • DolphinScheduler 3.2.0 Worker启动核心源码解析
  • 一天一道Sql题(day05)
  • IntelliJ IDEA 2025.1.3创建不了java8的项目
  • 初识MySQL(三)之主从配置与读写分离实战
  • Mac电脑,休眠以后,发现电量一直在减少,而且一个晚上,基本上是没了,开机都需要插电源的简单处理
  • Hive MetaStore的实现和优化
  • 在 macOS 上安装与自定义 Oh My Zsh:让终端美观又高效 [特殊字符]
  • 如何使用Pytest进行测试?
  • 基于大模型的窦性心动过速全周期预测与诊疗方案研究报告
  • 【linux】ssh使用-X参数后报错:X11 forwarding request failed on channel 0