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

C++线程池学习 Day06

任务:延续昨天的内容,理解内存序

内存序 std::memory_order

你和你的朋友(代表不同的CPU线程)在合作完成一个巨大的拼图。你们之间隔着一堵墙,只能通过一个小窗口传递拼图块(这代表线程间通信开销很大)

1.默认情况(std::memory_order_seq_cst):每当你拼上一块拼图,你就必须立刻走到窗口前,对你的朋友大声说:“我刚刚拼上了A块!”。然后你才能回去拼下一块。这样做的结果很明确,这使得你们对任务的进度有完全一致的认知,但效率低下,因为大部分时间都花在通知上

2.宽松模式(std::memory_order_relaxed):你只管埋头拼命拼图。只有当你手头所有拼图块用完了,或者完成了一大片区域,你才会走到窗口前说:“我拼了很多,具体哪些我也记不清了,你自己看吧!“。这样效率极高,但是你的朋友根本不知道你拼图的顺序,它对进度的认知可能是混乱的

3.释放-获取模式(std::memory_order_release/acquire):你拼好了一块非常关键的中心块(释放操作),拿着这块中心块走到窗口前说:“注意,在我交出这块中心块之前,我完成的所有拼图(例如一片蓝色的天空)都是有效的!”。然后你的朋友接过中心块(获取操作),他听到你的话后就明白了:”既然我拿到了这块中心块,那我就确信他刚才说的那片蓝色天空一定拼好了。“这种方式在关键点上同步,在其它地方则保持高效,是一种聪明的合作方式

在这个比喻中:

”拼图块“就是普通的内存读写操作

”走到窗口前说话“就是一个原子操作

”说话的内容和方式“就是内存序。它定义了这次沟通要传递多少信息、保证什么样的顺序

再举一个例子:假设我们有两个变量:一个普通变量data(装着重要答案)和一个原子变量flag(作为发布信号的哨兵)

// 线程 1: 生产者/发布者
data = 42;                          // (1) 准备答案,这是一个普通写操作
flag.store(true, std::memory_order_release); // (2) RELEASE操作:发布信号!

昨天说过,编译器会对我们写的代码进行优化。如果没有std::memory_order_release,编译器或CPU可能会觉得:我先执行(2),再执行(1)也没关系啊,反正逻辑不变,说不定还会更快呢?于是进行了代码重排。如果在多线程环境下,这是危险的。如果另一个线程在flag变成true时来读data,它可能会读到陈旧的、错误的data

有了std::memory_order_release,相当于对编译器和CPU下了一个严格的命令:不管怎么优化,必须保证操作(1)的效果在操作(2)之前完成!

我们再看它的另一半:Acquire

// 线程 2: 消费者/获取者
if (flag.load(std::memory_order_acquire)) { // (3) ACQUIRE操作:检查信号print(data);                          // (4) 使用答案
}

std::memory_order_acquire保证acquire操作之后的所有内存操作,不会被重排到它之前

也就是说,(4)一定不会被重排到操作(3)之前执行

现在,我们把两个线程的代码结合起来看

线程1:data写入->release存储flag

线程2:acquire加载flag->data读取

当线程2通过acquire加载flag并看到它的值为true,即线程1的release存储已经完成时,这两个内存序之间就建立了一个同步关系。就好比上面的拼图比喻,当对方接收到你给的中心块时,你们对整个任务的进度的认知是同步的

即,线程1在release操作之前所做的所有事情,其结果对线程2在acquire操作之后的所有操作都是可见的。更具体地说,线程2不需要亲眼去看着线程1完成任务,因为它相信,只要flag为true,那么线程1的任务就必然完成了。所谓的可见性就是,线程2发现flag为true,它就知道线程1的工作做完了,本质是认知同步。这种信任是由内存序的规则背书的。

所以内存序是什么?它是一个规则,让你可以精确控制原子操作周围的其它内存操作(读、写)的顺序和可见性,让你能够与编译器和CPU进行对话,告诉它们这个位置是否需要严格的顺序(seq_cst),或者这个位置的顺序无所谓,可以随便优化(relaxed)

内存序通过规定 操作的执行顺序,要求在关键点必须进行同步。为了在硬件层面实现这些同步,CPU必须执行缓存一致性操作(如使缓存失效、刷写内存),从而解决了缓存不一致的问题,进而保证了一个线程的修改都能够被另一个线程知道。所谓的可见性,本质上就是通过内存序触发CPU执行缓存一致性操作后的结果,是通过确保缓存一致实现的。

再用一个比喻理解:

科学家A规定:“在我发布胜利宣言(flag.store(true,release))之前,我必须把我的发现正式提交到中央档案库(刷新缓存),并通知其它科学家,他们笔记本上关于这个定理的记录已经作废(缓存失效)。”

科学家B也规定:“在我看到胜利宣言之后,我必须从中央档案库获取最新资料,确保我的认知是最新的(认知同步)。”

接下来看一下六个内存序

1️⃣std::memory_order_relaxed(宽松)

昨天说过,它只保证自己的操作本身是原子的,不保证该操作之前或之后的任何其它内存操作的顺序

也就是说,如果对于原子变量flag,我们执行:

flag.store(true,std::memory_order_relaxed);

它唯一的作用就是保证了flag=true这条语句被原子地执行。由Day02的总结,我们知道flag=true并不是一个原子操作。加上std::memory_order_relaxed后,它能保证这条语句被原子地执行

至于它的上下文是否会重排到它的前面或者后面,它并不关心。

用一个具体的例子加深理解:

int data = 0;
std::atomic<int> counter(0);
​
void thread1() {data = 42;                     // (1) 普通写操作counter.store(1, std::memory_order_relaxed); // (2) relaxed存储int b = data;                  // (3) 普通读操作
}

对于线程thread1自己来说,它永远会看到b=42

但是对于其它观察者(其它线程)来说,他们看到的顺序可能是混乱的

可能的情况1:编译器/CPU先执行2,再执行1

counter.store(1, std::memory_order_relaxed); // (2) 先发生
data = 42;                                   // (1) 后发生
int b = data;                                // (3)

可能的情况2:

int b = data;                                // (3) 先发生(读到旧值0)
counter.store(1, std::memory_order_relaxed); // (2)
data = 42;                                   // (1)

std::memory_order_relaxed允许所有这些可能的重排。它只关心自己的counter.store操作原子地完成了

2️⃣std::memory_order_seq_cst(顺序一致性)

它保证了全局顺序一致性:所有线程看到的所有seq_cst操作的顺序都是一致的。即,对于两个seq_cst操作A和B,所有线程对于A和B谁先执行有统一的认知

它隐式地包含了acquire和release的语义:

seq_cst_load≈acquire_load+参与全局排序

seq_cst_store≈release_store+参与全局排序

怎么理解它隐式包含了acquire和release的语义?

seq_cst和acquire一样,在自己所在的线程中,确保在自己之后的必然在自己之后执行

seq_cst和release一样,在自己所在的线程中,确保在自己之前的必然在自己之前执行

比如说在线程1中,seq_cst语句A写在了seq_cst语句B前面,那么它保证了先A后B。在线程2中用同样的方法保证了先C后D。但是从全局来看,A与C和D的先后是不确定的,B与C和D的先后也是不确定的。

可能的顺序有几种?这就变成了一个排列问题了:ABCD四人排队,其中A在B前面,C在D前面,有几种排法?答案是

但是最终只会选择一种排法,并且所有线程都达成了ABCD四个线程就应该这么排的共识

3️⃣std::memory_order_release

4️⃣std::memory_order_acquire

这两个不再赘述,要配对使用才能发挥效果。release用于store,保证在我之前的必然在我之前执行,acquire用于load,保证在我之后的必然在我之后执行。这是构建线程同步的黄金组合

5️⃣std::memory_order_acq_rel

顾名思义,是acquire和release的结合体,用于Read-Modify-Write(RMW)操作(如昨天的fetch_add)

对于RMW操作之后的内存操作,保证它必然在后面执行

对于RMW操作之前的内存操作,保证它必然在前面执行

6️⃣std::memory_order_consume

c++20已经弃用,一般使用acquire替代它

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

相关文章:

  • React 样式CSS的定义 多种定义方式 前端基础
  • react+anddesign组件Tabs实现后台管理系统自定义页签头
  • Midscene 低代码实现Android自动化
  • ADB使用指南
  • FunCaptcha如何查找sitekey参数
  • 大模型如何让机器人实现“从冰箱里拿一瓶可乐”?
  • Python实现液体蒸发优化算法 (Evaporation Rate Water Cycle Algorithm, ER-WCA)(附完整代码)
  • MySQL 数据库的「超级钥匙」—`mysql_real_connect`
  • LeetCode 每日一题 3484. 设计电子表格
  • RAGAS深度解析:引领RAG评估新时代的开源技术革命
  • aave v3.4 利率计算详解
  • rook-ceph CRD资源配置时效问题
  • MySQL学习笔记-进阶篇
  • Rust 关键字
  • 排版使用latex排版还是word排版更容易通过mdpi remote sensing的审稿?
  • Qt QML ToolTip弹出方向控制问题探讨
  • [Windows] PDFQFZ(PDF加盖骑缝章) v1.31
  • 四网络层IP-子网掩码-路由表-真题
  • 安装QT6.9.2
  • 使用 NodePort
  • IP6163至为芯具备MPPT硬件算法的太阳能光伏降压DC-DC芯片
  • 从“道生一”理念看宇宙规律与现代科技之关联
  • CKS-CN 考试知识点分享(9) 关闭API凭据自动挂载
  • 初次接触MCP
  • 高防服务器按照应用场景划分为哪些类型
  • 【项目】基于One Thread One Loop模型的高性能网络库实现 - 服务器模块实现
  • 京准电钟NTP时间同步服务器通信系统技术应用方案
  • Next.js 错误处理:自定义错误页面和错误边界
  • 操作教程|使用Cursor工具连接JumpServer资产
  • prefix Lm和causal LM encoder-decoder区别以及各自有什么缺点