当前位置: 首页 > 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.lryc.cn/news/583531.html

相关文章:

  • Token 和 Embedding的关系
  • 通过Tcl脚本命令:set_param labtools.auto_update_hardware 0
  • AI Agent:我的第一个Agent项目
  • 在 macOS 上安装与自定义 Oh My Zsh:让终端美观又高效 [特殊字符]
  • css支持if else
  • WIndows 编程辅助技能:格式工厂的使用
  • 单片机STM32F103:DMA的原理以及应用
  • React面试高频考点解析
  • 【LeetCode 热题 100】21. 合并两个有序链表——(解法二)递归法
  • Spark流水线数据对比组件
  • 第6章应用题
  • 01-elasticsearch-搭个简单的window服务-ik分词器-简单使用
  • 【01】MFC入门到精通—— MFC新建基于对话框的项目 介绍(工作界面、资源视图 、类视图)
  • 【前端】ikun-markdown: 纯js实现markdown到富文本html的转换库
  • Java SE 实现简单的图书管理系统(完善菜单操作)
  • 【DOCKER】-3 数据持久化
  • 项目进度受制于资源分配,如何动态调配资源
  • 20250709: WSL+Pycharm 搭建 Python 开发环境
  • PHP 基于模板动态生成 Word 文档:图片 + 表格数据填充全方案(PHPOffice 实战)
  • 爬虫-数据解析
  • 20-C#构造函数--虚方法
  • 机器视觉之工业相机讲解
  • 【leetcode100】下一个排列
  • 题解:P13017 [GESP202506 七级] 线图
  • RAC-CELL(小区)处理
  • 射频前端的革新力量:ATR2057超低噪声放大器深度解析
  • C#基础篇(10)集合类之列表
  • AMIS全栈低代码开发
  • Claude Code 开发使用技巧
  • 一天一道Sql题(day05)