内存模型的基础知识
内存位置是什么?
引用cppreference.com中对内存位置的定义:
- 标量对象(算术类型、指针类型、枚举类型或
std::nullptr_t), - 或非零长度的连续序列。
下面是内存位置的例子:
cpp
struct S {
char a; // memory location #1
int b : 5; // memory location #2
int c : 11, // memory location #2 (continued)
: 0,
d : 8; // memory location #3
int e; // memory location #4
double f; // memory location #5
std::string g; // several memory locations
};
首先,对象obj由七个子对象组成,其中b、c两个位字段共享内存位置。
观察上述结构体定义,可以得到如下结论:
- 每个变量都是一个对象。
- 标量类型占用一个内存位置。
- 相邻的位字段(b和c)具有相同的内存位置。
- 变量至少占用一个内存位置。
你想要通过汇编代码分析该带位域、混合基础类型与C++类对象的结构体内存模型,我会先明确布局理论基础,再通过可落地的汇编编译与分析流程,验证结构体的内存分布、对齐规则、位域特性。
1、分析前提(关键!影响内存布局)
我们基于主流编译环境展开分析,避免因环境差异导致结论偏差:
- 架构:x86_64(64位处理器,桌面端/服务器主流架构)
- 编译器:GCC(g++,默认编译选项,未开启
-fpack-struct(紧凑打包)) - 基础类型大小:
char=1字节、int=4字节、double=8字节 - 对齐规则:默认自然对齐(基本类型对齐到自身大小的整数倍,结构体整体对齐到最大成员对齐数的整数倍)
- 位域规则:基于
int(4字节=32位)的位域,仅占用整数类型的部分二进制位,匿名位域:0强制结束当前位域单元
2、先拆解结构体:理论内存布局(汇编验证的基础)
先手动分析每个成员的内存占用、填充(padding)与布局,为后续汇编分析提供对照:
| 成员 | 类型/位域 | 核心特性 | 理论占用/对齐 | 对应内存位置说明(修正用户标注的细节) |
|---|---|---|---|---|
| char a | 字符类型 | 1字节,后续紧跟int位域,需填充3字节 (对齐到int的4字节起始地址) |
占用1字节 + 填充3字节(共4字节) | 对应memory location #1(含填充) |
| int b:5 | 位域(5位) | 基于int(32位),占用5位,无偏移,紧跟在对齐后的地址后 |
占用5位(同1个int单元) |
对应memory location #2 |
| int c:11 | 位域(11位) | 紧跟b,占用11位,b+c=16位,仍在同一个int(32位)单元内 |
占用11位(同1个int单元) |
对应memory location #2(延续) |
| :0 | 匿名位域(0位) | 强制结束当前位域单元(当前int),剩余16位(32-16)被舍弃,不占用有效位 |
无实际占用,仅用于位域截断 | 分割c和d的位域单元 |
| int d:8 | 位域(8位) | 开启新的int位域单元(4字节),占用8位,剩余24位被舍弃 |
占用8位(新int单元,4字节) |
对应memory location #3 |
| int e | 完整int类型 | 4字节,无需额外填充(前序成员已对齐到4字节) | 4字节 | 对应memory location #4 |
| double f | 双精度浮点型 | 8字节,需对齐到8字节边界(需检查前序总大小,必要时填充4字节) | 8字节 | 对应memory location #5 |
| std::string g | C++字符串对象 | 非基础类型,GCC中默认是3个8字节指针(24字节),内部指向堆上字符数据 | 24字节(结构体自身占用) | 多个连续内存单元,堆内存额外占用 |
3、步骤1:编写测试代码(用于生成汇编)
创建struct_test.cpp,定义结构体并主动访问所有成员(避免编译器优化掉未使用成员,确保汇编中出现成员偏移):
cpp
#include <string>
// 原结构体定义
typedef struct S {
char a; // memory location #1
int b : 5; // memory location #2
int c : 11, // memory location #2 (continued)
: 0, // 强制结束当前位域单元
d : 8; // memory location #3
int e; // memory location #4
double f; // memory location #5
std::string g; // several memory locations
} sss;
int main() {
// 定义结构体变量,分配栈内存(便于汇编分析偏移)
sss test;
// 主动访问所有成员,触发内存操作指令
test.a = 'A';
test.b = 3;
test.c = 10;
test.d = 7;
test.e = 100;
test.f = 3.14159;
test.g = "Hello, GitHub";
return 0;
}
4、步骤2:编译生成汇编代码
使用g++编译,生成易读的AT&T格式汇编(关闭优化,保留完整内存操作痕迹):
bash
# -S:生成汇编代码(不编译成二进制)
# -O0:关闭编译器优化(关键!避免成员访问被重排/消除)
# -o struct_test.s:指定汇编输出文件名
g++ -S -O0 struct_test.cpp -o struct_test.s
执行后,当前目录会生成struct_test.s文件(纯文本格式,可直接用记事本/VS Code打开)。
5、步骤3:汇编代码分析(验证内存模型)
我们重点分析main函数中结构体成员的内存偏移地址(核心依据),结合指令含义验证理论布局:
5-1. 核心观察点:结构体变量的栈分配与成员偏移
在struct_test.s中找到main函数(关键字main:),重点看mov系列指令(内存写入操作),示例汇编片段(GCC生成,略有差异但核心一致):
z80
main:
pushq %rbp # 栈帧初始化
movq %rsp, %rbp
subq $80, %rsp # 分配80字节栈空间给test(结构体总大小,含填充)
# 1. 访问 test.a(char,偏移 0)
movb $65, -80(%rbp) # 'A'的ASCII码是65,-80(%rbp)是test的基地址+0(a的偏移)
# 验证:a占用偏移0,1字节,对应memory location #1
# 2. 访问 test.b(位域5位,偏移 4)
movl $3, -76(%rbp) # -76(%rbp) = 基地址+4(a填充3字节后,对齐到4字节偏移)
andl $31, -76(%rbp) # 31是0b11111,掩码提取5位(验证b占5位)
# 验证:b在偏移4的int单元(memory location #2)
# 3. 访问 test.c(位域11位,偏移 4,紧跟b)
movl -76(%rbp), %eax
shll $11, %eax # 左移11位,为c分配空间
orl $10, %eax # 写入c的值10
movl %eax, -76(%rbp) # 写回偏移4的int单元
andl $0x7FF, %eax # 0x7FF=11位全1,掩码提取c(验证c占11位)
# 验证:c与b同属偏移4的int单元(memory location #2)
# 4. 访问 test.d(位域8位,偏移 8)
movl $7, -72(%rbp) # -72(%rbp) = 基地址+8(新的int单元,验证:0截断了前一个位域)
andl $255, -72(%rbp) # 255=0b11111111,掩码提取8位(验证d占8位)
# 验证:d在偏移8的int单元(memory location #3)
# 5. 访问 test.e(int,偏移 12)
movl $100, -68(%rbp) # -68(%rbp) = 基地址+12(4字节对齐,无填充)
# 验证:e占用偏移12-15(4字节,memory location #4)
# 6. 访问 test.f(double,偏移 16)
movsd .LC0(%rip), %xmm0 # 加载3.14159的浮点值
movsd %xmm0, -64(%rbp) # -64(%rbp) = 基地址+16(8字节对齐,验证前序总大小16字节,无需填充)
# 验证:f占用偏移16-23(8字节,memory location #5)
# 7. 访问 test.g(std::string,偏移 24)
leaq .LC1(%rip), %rdi # 加载字符串常量地址
leaq -56(%rbp), %rsi # -56(%rbp) = 基地址+24(g的起始偏移)
call std::basic_string<char>::assign(...) # 调用string赋值函数
# 验证:g从偏移24开始,占用24字节(3*8字节),对应多个内存单元
5-2. 汇编分析核心结论(对应内存模型)
| 汇编观察点 | 内存模型验证结果 |
|---|---|
test.a偏移0 |
char a占用1字节(偏移0),后续偏移1-3为填充字节(3字节),对齐到int的4字节边界 |
test.b/c偏移4 |
b(5位)+c(11位)共16位,同属偏移4-7的int单元(4字节),验证memory location #2 |
test.d偏移8 |
匿名位域:0强制截断偏移4的int单元,d在偏移8-11的新int单元,验证位域截断特性 |
test.e偏移12 |
完整int,4字节对齐,无填充,验证memory location #4 |
test.f偏移16 |
double占用8字节,对齐到16字节边界(前序总大小16字节),验证memory location #5 |
test.g偏移24 |
std::string占用24字节(偏移24-47),内部通过指针指向堆内存,验证"多个内存位置" |
subq $80, %rsp |
结构体总大小80字节(含所有填充),对齐到double的8字节边界(80是8的整数倍) |
5-3. 特殊成员补充分析
- 匿名位域
:0:汇编中d的偏移为8(而非偏移4的延续),直接证明:0的作用------强制结束当前位域单元(偏移4的int),即使该单元还有16位空闲,也会舍弃,下一位域必须开启新单元。 - 位域访问方式 :汇编中无直接操作"位"的指令,而是通过
movl加载整个int单元,再用andl(掩码提取)、shll(移位)、orl(位写入)操作位域,验证位域是"基于整数类型的位分割"。 - std::string g:汇编中仅操作结构体自身的24字节(指针/大小信息),实际字符数据存储在堆上,结构体中仅保存堆内存的引用,验证"several memory locations"(结构体内存+堆内存)。
6、补充:不同环境的差异说明
- 若使用
MSVC编译器:std::string大小可能为16字节(2个8字节指针),结构体总大小略有变化,但位域核心规则不变; - 若开启
-fpack-struct=1(紧凑打包):char a后无填充字节,a占用1字节,b直接从偏移1开始,总大小减小; - 32位x86架构:
std::string指针为4字节,占用12字节,结构体总大小相应减小。
总结
- 该结构体的内存模型受位域规则 (
:0截断)、自然对齐 (char填充3字节、double8字节对齐)、类型大小(int4字节、double8字节)共同影响; - 汇编代码通过成员偏移地址 和位运算指令 ,精准验证了理论布局,位域成员依赖整数单元+位运算访问,
std::string区分结构体自身内存与堆内存; - 核心关键:匿名位域
:0强制结束位域单元、基础类型对齐要求、非基础类型(std::string)的内存分布特性。
两个线程访问相同的内存位置,会发生什么呢?
如果两个线程访问相同的内存位置(相邻位字段共享内存位置),并且至少有一个线程想要修改它,那么程序就会产生数据竞争,除非:
- 修改操作为原子操作。
- 访问按照某种先行(happens-before)顺序进行。
第二种情况非常有趣,同步语义(如互斥锁)可以建立了先行关系。这些先行关系基于原子建立,当然也适用于非原子操作。内存序(memory-ordering)是内存模型的关键部分,其定义了先行关系的细节。
强弱内存模型
你想要深入理解C++的强内存模型与弱内存模型,包括它们的核心定义、差异以及实际代码体现,我会先从理论层面清晰拆解,再通过可运行的C++代码直观演示两者的特性。
1、核心概念铺垫
C++的内存模型本质是定义多线程环境下内存操作的可见性、有序性和原子性规则,"强"与"弱"的核心区别在于对「内存操作重排序」和「跨线程可见性」的约束严格程度:
- 重排序:编译器(编译优化)或CPU(执行优化)可能打乱代码的执行顺序(满足as-if规则:单线程内行为不变),但跨线程可能引发问题
- 可见性:一个线程对内存的修改,能否被其他线程及时观察到
2、强内存模型(Sequential Consistency,顺序一致模型)
1. 核心定义
强内存模型是最严格的内存模型,满足两个关键特性:
- 单个线程内:所有内存操作严格按照代码的书写顺序执行(无指令重排,或重排对程序员完全透明)
- 跨线程间:所有线程看到的「全局内存操作顺序」是完全一致的,就像存在一个"全局操作队列",所有线程的内存操作都按顺序入队执行,无乱序可见性问题
2. 关键特性
- 编程简单:无需手动添加内存屏障或显式指定内存顺序,天然避免大部分多线程内存问题
- 性能损耗:约束了编译器和CPU的优化空间(无法进行激进的重排序优化),在多核架构下性能略低
- C++中的体现:
std::atomic原子操作的默认内存顺序 (memory_order_seq_cst)就是强内存模型,这是C++为原子操作提供的最严格内存顺序
3. 强内存模型代码示例
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <cassert>
// 全局原子变量(默认内存顺序:memory_order_seq_cst,强内存模型)
std::atomic<int> x(0);
std::atomic<int> y(0);
// 用于存储跨线程观察到的值
int a = 0;
int b = 0;
// 线程1:先修改x,再修改y
void thread1() {
x.store(1); // 默认memory_order_seq_cst,强内存模型
y.store(1); // 严格在x.store之后执行(单线程内有序)
}
// 线程2:先读取y,再读取x
void thread2() {
while (y.load() != 1); // 等待y被修改为1(强模型下,y=1必然意味着x=1已完成)
b = y.load();
a = x.load(); // 必然能读取到x=1,无乱序问题
}
int main() {
// 多次运行验证(强模型下结果稳定)
for (int i = 0; i < 10000; ++i) {
// 重置变量
x = 0;
y = 0;
a = 0;
b = 0;
// 创建并运行线程
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
// 断言验证:a必然为1,b必然为1
assert(a == 1 && b == 1);
std::cout << "第" << i+1 << "次运行:a=" << a << ", b=" << b << std::endl;
}
std::cout << "强内存模型下,所有运行均满足断言(无乱序问题)" << std::endl;
return 0;
}
4. 代码说明
- 编译命令:
g++ -std=c++11 -pthread strong_memory_model.cpp -o strong_model(需支持C++11及以上,链接线程库) - 运行结果:所有循环中
a必然为1,b必然为1,断言永不触发 - 核心原因:强内存模型(
memory_order_seq_cst)保证:
- 线程1中
x.store(1)严格在y.store(1)之前执行(单线程内有序) - 线程2中观察到
y=1时,必然能观察到x=1(跨线程全局顺序一致,x的修改先于y,对所有线程可见)
输出结果如下
cpp
第1205次运行:a=1, b=1
第1206次运行:a=1, b=1
第1207次运行:a=1, b=1
第1208次运行:a=1, b=1
第1209次运行:a=1, b=1
第1210次
[Truncated]
3、弱内存模型
1. 核心定义
弱内存模型对内存操作的约束更为宽松,允许编译器和CPU进行更多激进优化(重排序、存储缓冲等),以提升多核架构下的性能,其核心特性:
- 单个线程内:满足as-if规则(看似按代码顺序执行),但实际可能发生重排序(对其他线程可见)
- 跨线程间:不同线程可能看到不同的「全局内存操作顺序」,无统一的全局操作队列
- 需显式约束:必须通过内存顺序参数 (如
memory_order_relaxed、memory_order_acquire/memory_order_release)或内存屏障,手动保证关键操作的可见性和有序性
2. C++中弱内存模型的常见类型
| 内存顺序参数 | 核心特性(弱内存模型范畴) | 适用场景 |
|---|---|---|
memory_order_relaxed |
最宽松:仅保证原子性,无有序性和可见性约束 | 独立计数器(如统计访问次数) |
memory_order_acquire |
读操作:禁止后续操作重排到该操作之前,保证能看到对应release的修改 |
读取共享资源 |
memory_order_release |
写操作:禁止前面操作重排到该操作之后,保证修改对对应acquire可见 |
修改共享资源并释放锁 |
3. 弱内存模型代码示例1:最宽松的memory_order_relaxed(演示乱序问题)
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
// 全局原子变量(使用弱内存模型:memory_order_relaxed)
std::atomic<int> x(0);
std::atomic<int> y(0);
// 统计异常情况:y=1但x=0的次数(弱模型下会出现)
int abnormal_count = 0;
// 线程1:代码顺序:x=1 → y=1(弱模型下可能被重排)
void thread1() {
x.store(1, std::memory_order_relaxed); // 仅保证原子性,无有序性约束
y.store(1, std::memory_order_relaxed);
}
// 线程2:代码顺序:读y → 读x(弱模型下可能观察到y=1但x=0)
void thread2() {
while (y.load(std::memory_order_relaxed) != 1); // 等待y=1
int y_val = y.load(std::memory_order_relaxed);
int x_val = x.load(std::memory_order_relaxed); // 可能读取到x=0(乱序可见)
if (y_val == 1 && x_val == 0) {
++abnormal_count; // 统计异常情况
}
}
int main() {
const int test_times = 100000; // 多次运行放大异常概率
std::vector<std::thread> threads;
for (int i = 0; i < test_times; ++i) {
// 重置变量
x = 0;
y = 0;
// 创建线程
std::thread t1(thread1);
std::thread t2(thread2);
// 等待线程结束
t1.join();
t2.join();
}
std::cout << "弱内存模型(memory_order_relaxed)测试完成,共运行" << test_times << "次" << std::endl;
std::cout << "异常情况(y=1但x=0)出现次数:" << abnormal_count << std::endl;
if (abnormal_count > 0) {
std::cout << "验证成功:弱内存模型下存在跨线程乱序可见性问题" << std::endl;
} else {
std::cout << "本次运行未出现异常(弱模型乱序概率与CPU架构相关,可增加测试次数重试)" << std::endl;
}
return 0;
}
cpp
弱内存模型(memory_order_relaxed)测试完成,共运行100000次
异常情况(y=1但x=0)出现次数:0
本次运行未出现异常(弱模型乱序概率与CPU架构相关,可增加测试次数重试)
4. 弱内存模型代码示例2:释放-获取语义(release/acquire,约束弱模型)
memory_order_relaxed过于宽松,实际开发中常用release/acquire(弱模型的安全用法),既保留性能优势,又能保证关键操作的有序性和可见性:
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
// 原子标志:用于同步共享数据(释放-获取语义)
std::atomic<bool> flag = ATOMIC_VAR_INIT(false);
// 共享数据(非原子,需通过release/acquire保证可见性)
std::string shared_data = "init";
// 线程1:生产者,修改共享数据后释放标志
void producer() {
shared_data = "Hello, Weak Memory Model!"; // 普通写操作
// release语义:禁止前面的操作(shared_data修改)重排到该操作之后
// 保证shared_data的修改对后续acquire该flag的线程可见
flag.store(true, std::memory_order_release);
}
// 线程2:消费者,获取标志后读取共享数据
void consumer() {
// acquire语义:禁止后续操作(shared_data读取)重排到该操作之前
// 保证能看到flag.store(release)之前的所有内存修改
while (!flag.load(std::memory_order_acquire));
// 断言:必然能读取到生产者修改后的共享数据
assert(shared_data == "Hello, Weak Memory Model!");
std::cout << "消费者读取到共享数据:" << shared_data << std::endl;
}
int main() {
std::thread t_prod(producer);
std::thread t_cons(consumer);
t_prod.join();
t_cons.join();
std::cout << "释放-获取语义(弱内存模型):同步成功,断言未触发" << std::endl;
return 0;
}
plain
消费者读取到共享数据:Hello, Weak Memory Model!
释放-获取语义(弱内存模型):同步成功,断言未触发
5、弱内存模型代码说明
- 编译命令:
g++ -std=c++11 -pthread weak_memory_model.cpp -o weak_model - 示例1(
relaxed):- 运行结果:在ARM、PowerPC等弱内存架构下,
abnormal_count会大于0(观察到乱序);在x86_64架构下,由于硬件本身接近强内存模型,乱序概率较低,可增加测试次数(如100万次)或使用-O2优化触发编译器重排 - 核心现象:弱模型下,线程2可能观察到
y=1但x=0,这是强模型下绝不会出现的情况
- 运行结果:在ARM、PowerPC等弱内存架构下,
- 示例2(
release/acquire):- 运行结果:断言永不触发,消费者必然能读取到生产者修改后的
shared_data - 核心原理:
release与acquire形成"同步关系",既保留了弱模型的性能优势,又避免了乱序和可见性问题,是弱内存模型的主流用法
- 运行结果:断言永不触发,消费者必然能读取到生产者修改后的
4、强内存模型 vs 弱内存模型 核心对比
| 对比维度 | 强内存模型(memory_order_seq_cst) | 弱内存模型(relaxed/acquire/release) |
|---|---|---|
| 重排序允许 | 禁止跨线程可见的重排序(全局有序) | 允许非关键操作的重排序(宽松约束) |
| 跨线程可见性 | 天然保证,无需额外约束 | 需显式通过内存顺序参数保证 |
| 编程复杂度 | 低(无需关注内存顺序,不易出错) | 高(需手动选择内存顺序,易踩坑) |
| 性能 | 较低(约束优化,有全局同步开销) | 较高(宽松约束,充分利用硬件/编译器优化) |
| 典型使用场景 | 简单同步场景(如小计数器、状态标志) | 高性能要求的复杂多线程场景(如并发容器、锁实现) |
| 对应C++内存顺序 | memory_order_seq_cst(默认) | memory_order_relaxed/acquire/release |
总结
- 强内存模型(顺序一致):严格约束,编程简单,性能略低,对应
memory_order_seq_cst,天然保证有序性和可见性; - 弱内存模型:宽松约束,性能更高,编程复杂,需显式指定内存顺序(
relaxed/acquire/release),是高性能并发编程的核心; - 实际开发中:优先使用强内存模型(默认原子顺序)保证正确性,在性能瓶颈处,使用
release/acquire(弱模型)优化,避免直接使用memory_order_relaxed(风险极高); - 架构差异:x86_64硬件接近强内存模型,弱模型的乱序现象不明显;ARM、PowerPC等架构严格遵循弱内存模型,需重点关注内存顺序约束。
原子标志
std::atomic_flag是原子布尔类型,可以对其状态进行设置和清除。为了简化说明,我将clear状态称为false,将set状态称为true。clear方法可将其状态设置为false。test_and_set方法,可以将状态设置回true,并返回先前的值。这里,没有方法获取当前值。使用std::atomic_flag时,必须使用常量ATOMIC_FLAG_INIT将std::atomic_flag初始化为false。
ATOMIC_FLAG_INIT
std::atomic_flag需要初始化时,可以是这样:std::atomic_flag flag = ATOMIC_FLAG_INIT。不过,不能这样进行初始化:
std::atomic_flag flag(ATOMIC_FLAG_INIT)。
std::atomic_flag有两个特点:
- 无锁原子类型。程序是系统级别进程的话,执行的非阻塞算法就是无锁的。
- 更高级别的线程构建块。
除了std::atomic_flag之外,C++标准中的原子内部都会使用互斥锁。这些原子类型有一个is_lock_free成员函数,可用来检查原子内部是否使用了互斥锁。时下主流的微处理器架构上,都能得到"使用了互斥锁"的结果。如果想要无锁编程,那么就要使用该成员函数进行检查,确定是否使用了锁。
std::is_always_lock_free可以使用
obj.is_lock_free(),在运行时检查原子类型的实例obj是否无锁。在C++17中,可以通过constexpr(常量)atomic<type>::is_always_lock_free,在编译时对每个原子类型进行检查,支持该操作的所有硬件实现都无锁时,才返回true。
std::atomic_flag的接口非常强大,能够构建自旋锁。自旋锁可以像使用互斥锁一样保护临界区。
自旋锁
**自旋锁与互斥锁不同,它并不获取锁。而是,通过频繁地请求锁来获取临界区的访问权。不过,这会导致上下文频繁切换(从用户空间到内核空间),虽然充分使用了CPU,但也浪费了非常多的时钟周期。线程短时间阻塞时,自旋锁非常有效。**通常,会将自旋锁和互斥锁组合着使用。首先,在有限的时间内使用自旋锁;如果不成功,则将线程置于等待(休眠)状态。
自旋锁不应该在单处理器系统上使用。否则,自旋锁就不仅浪费了资源,而且还会减慢程序处理的速度(最好的情况),或出现死锁(最坏的情况)。
下面的代码,使用std::atomic_flag实现了自旋锁。
cpp
// spinLock.cpp
#include <atomic>
#include <thread>
#include <iostream>
using namespace std;
class Spinlock{
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock(){
while(flag.test_and_set());
}
void unlock(){
flag.clear();
}
};
Spinlock spin;
void workOnResource(){
spin.lock();
// shared resource
std::thread::id current_id = std::this_thread::get_id();
cout << "id " << current_id << " locking ..."<< endl;
spin.unlock();
}
int main(){
std::thread t(workOnResource);
std::thread t2(workOnResource);
t.join();
t2.join();
}
线程t和t2(第31行和第32行)在争夺临界区的访问权。这里的自旋锁是如何工作的呢?自旋锁也有锁定和解锁的阶段。
当线程t执行函数workOnResource时,可能会发生以下情况:
- 线程
t获取锁,若第11行的标志初始值为false,则锁调用成功。这种情况下,线程t的原子操作将其设置为true。所以,当t线程获取锁后,true将会让while陷入死循环,使得线程t2陷入了激烈的竞争当中。线程t2不能将标志设置为false,因此t2必须等待,直到线程t1执行unlock(解锁)并将标志设置为false(第14 - 16行)时,才能获取锁。 - 线程
t没有得到锁时,情况1中的t2一样,需要等待。
输出如下:
cpp
id 129839973529152 locking ...
id 129839965136448 locking ...
我们将注意力放在std::atomic_flag的test_and_set成员函数上。test_and_set函数包含两个操作:读和写。原子操作就是对这两种操作进行限制。如果没有限制,线程将对共享资源同时进行读和写(第24行),根据定义,这就属于"数据竞争",程序还会有未定义行为发生。
将自旋锁的主动等待和互斥锁的被动等待做一下比较。
自旋锁 vs. 互斥锁
如果函数workOnResource在第24行停顿2秒,那CPU负载会发生怎样的变化?
cpp
// spinLockSleep.cpp
#include <atomic>
#include <thread>
class Spinlock{
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock(){
while(flag.test_and_set());
}
void unlock(){
flag.clear();
}
};
Spinlock spin;
void workOnResource(){
spin.lock();
std::thread::id current_id = std::this_thread::get_id();
cout << "id " << current_id << " locking ..."<< endl;
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
spin.unlock();
}
int main(){
std::thread t(workOnResource);
std::thread t2(workOnResource);
t.join();
t2.join();
}
如下图所示,四个核中每次有一个是跑满了的。
我的PC上有一个核的负载达到100%,每次不同的核芯执行"忙等待"。
cpp
id 134537245881920 locking ...
id 134537237489216 locking ...
我现在用互斥锁来替换自旋锁。让我们看下会发生什么。
cpp
// mutex.cpp
#include <mutex>
#include <thread>
std::mutex mut;
void workOnResource(){
mut.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
mut.unlock();
}
int main(){
std::thread t(workOnResource);
std::thread t2(workOnResource);
t.join();
t2.join();
}
虽然执行了好几次,但是并没有观察到任何一个核上有显著的负载。
std::atomic模板
一、std::atomic模板核心概述
1. 基本定义
std::atomic是C++11引入的原子操作模板类 ,定义在<atomic>头文件中,用于实现多线程环境下的无锁并发访问。它的核心作用是保证对其封装的变量的操作是"原子性"的------操作不可分割,无中间状态,从而避免多线程数据竞争(Data Race),无需手动使用std::mutex等锁机制(轻量级同步方案)。
2. 核心特性
| 特性 | 详细说明 |
|---|---|
| 不可拷贝/不可移动 | 拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符均被显式delete,禁止拷贝/移动 |
| 模板参数支持 | 支持基本数据类型 (int/bool/char/指针等)、平凡可复制自定义类型 (需满足std::is_trivially_copyable) |
| 原子操作内置 | 提供存储、加载、交换、比较交换、算术运算、位运算等原子操作,无需手动加锁 |
| 内存顺序可配置 | 支持多种内存顺序参数(强/弱内存模型),兼顾正确性与性能 |
| 轻量级同步 | 相比std::mutex,无锁竞争的开销,性能更优(适用于简单同步场景) |
3. 头文件与编译要求
必须包含头文件:<atomic>
编译标准:C++11及以上
链接线程库:GCC/Clang需加 -pthread,MSVC自动链接
二、std::atomic 原子变量初始化
初始化是使用std::atomic的基础,需避免触发被删除的拷贝构造函数,以下是3种合法初始化方式:
方式1:直接初始化(推荐,C++11及以上通用)
使用圆括号()或大括号{}直接构造,跳过拷贝构造步骤,支持所有兼容类型。
cpp
#include <iostream>
#include <atomic>
int main() {
// 1. 圆括号直接初始化(数值类型)
std::atomic<int> atomic_int(10);
std::atomic<bool> atomic_bool(true);
std::atomic<char> atomic_char('a');
// 2. 大括号直接初始化(等价于圆括号,更现代的语法)
std::atomic<long> atomic_long{1000000L};
std::atomic<double> atomic_double{3.14159}; // 注:部分编译器对浮点型原子操作支持有限
// 3. 指针类型的直接初始化
int val = 100;
std::atomic<int*> atomic_ptr(&val);
// 输出初始化值
std::cout << "atomic_int = " << atomic_int.load() << std::endl;
std::cout << "atomic_bool = " << std::boolalpha << atomic_bool.load() << std::endl;
std::cout << "atomic_char = " << atomic_char.load() << std::endl;
std::cout << "atomic_long = " << atomic_long.load() << std::endl;
std::cout << "atomic_ptr 指向的值 = " << *atomic_ptr.load() << std::endl;
return 0;
}
编译命令:g++ -std=c++11 atomic_init1.cpp -o atomic_init1
cpp
atomic_int = 10
atomic_bool = true
atomic_char = a
atomic_long = 1000000
atomic_ptr 指向的值 = 100
方式2:ATOMIC_VAR_INIT 宏初始化(兼容老旧编译器/C++11早期版本)
专门为原子变量设计的初始化宏,绕过拷贝构造,直接初始化底层值,仅支持静态/全局原子变量或局部原子变量的常量初始化。
cpp
#include <iostream>
#include <atomic>
// 全局原子变量:使用ATOMIC_VAR_INIT宏初始化
std::atomic<int> global_atomic_int = ATOMIC_VAR_INIT(20);
std::atomic<bool> global_atomic_bool = ATOMIC_VAR_INIT(false);
int main() {
// 局部原子变量:使用ATOMIC_VAR_INIT宏初始化
std::atomic<char> local_atomic_char = ATOMIC_VAR_INIT('b');
std::cout << "global_atomic_int = " << global_atomic_int.load() << std::endl;
std::cout << "global_atomic_bool = " << std::boolalpha << global_atomic_bool.load() << std::endl;
std::cout << "local_atomic_char = " << local_atomic_char.load() << std::endl;
return 0;
}
编译命令:g++ -std=c++11 atomic_init2.cpp -o atomic_init2
cpp
global_atomic_int = 20
global_atomic_bool = false
local_atomic_char = b
方式3:默认构造 + store() 成员函数(延迟初始化场景)
先默认构造原子变量(此时无明确初始值),再通过 **store()**成员函数显式赋值,适合需要根据条件动态初始化的场景。
cpp
#include <iostream>
#include <atomic>
#include <string>
int main() {
// 步骤1:默认构造原子变量(不可拷贝,只能默认构造)
std::atomic<int> atomic_int;
std::atomic<std::string*> atomic_str_ptr; // 原子指针默认构造
// 步骤2:根据条件延迟初始化
bool need_init = true;
if (need_init) {
atomic_int.store(30, std::memory_order_seq_cst); // 强内存模型
std::string* str = new std::string("Hello atomic");
atomic_str_ptr.store(str, std::memory_order_relaxed); // 弱内存模型
}
// 读取值
std::cout << "atomic_int = " << atomic_int.load() << std::endl;
std::cout << "atomic_str_ptr 指向的值 = " << *atomic_str_ptr.load() << std::endl;
// 释放堆内存
delete atomic_str_ptr.load();
return 0;
}
编译命令:g++ -std=c++11 atomic_init3.cpp -o atomic_init3
cpp
atomic_int = 30
atomic_str_ptr 指向的值 = Hello atomic
三、std::atomic 核心原子操作
std::atomic提供了两类原子操作:成员函数操作 (灵活配置内存顺序)和重载运算符操作(简洁易用,默认强内存模型),以下是常用操作的详细说明与示例。
1. 存储操作:store() - 原子设置变量值
- 功能:原子性地将指定值存储到原子变量中,可指定内存顺序。
- 函数原型:
void store(T desired, std::memory_order order = std::memory_order_seq_cst); - 示例:
cpp
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> atomic_val(0);
// 线程函数:存储值到原子变量
void store_task(int val, std::memory_order order) {
atomic_val.store(val, order);
std::cout << "线程 " << std::this_thread::get_id() << " 存储值:" << val << std::endl;
}
int main() {
// 线程1:使用强内存模型存储
std::thread t1(store_task, 10, std::memory_order_seq_cst);
// 线程2:使用宽松内存模型存储
std::thread t2(store_task, 20, std::memory_order_relaxed);
t1.join();
t2.join();
// 最终值取决于线程执行顺序(原子操作保证无数据竞争)
std::cout << "最终 atomic_val = " << atomic_val.load() << std::endl;
return 0;
}
编译命令:g++ -std=c++11 atomic_store.cpp -o atomic_store -pthread
cpp
线程 130774487529024 存储值:10线程 130774479136320 存储值:20
最终 atomic_val = 20
2. 加载操作:load() - 原子获取变量值
- 功能:原子性地读取原子变量的当前值,可指定内存顺序。
- 函数原型:
T load(std::memory_order order = std::memory_order_seq_cst) const; - 示例:
cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <chrono>
std::atomic<int> atomic_val(100);
// 线程函数:循环加载原子变量值
void load_task() {
while (true) {
// 宽松内存模型加载,性能更高
int val = atomic_val.load(std::memory_order_relaxed);
std::cout << "线程 " << std::this_thread::get_id() << " 读取值:" << val << std::endl;
if (val == 0) break;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::thread t(load_task);
// 主线程500ms后将原子变量设为0,终止子线程
std::this_thread::sleep_for(std::chrono::milliseconds(500));
atomic_val.store(0, std::memory_order_relaxed);
t.join();
return 0;
}
编译命令:g++ -std=c++11 atomic_load.cpp -o atomic_load -pthread
3. 赋值运算符:operator= - 简洁存储(默认强内存模型)
- 功能:重载赋值运算符,内部调用
store(),默认使用std::memory_order_seq_cst内存顺序,语法更简洁。 - 示例:
cpp
#include <iostream>
#include <atomic>
int main() {
std::atomic<int> atomic_val(0);
// 等价于 atomic_val.store(5, std::memory_order_seq_cst)
atomic_val = 5;
std::cout << "赋值后 atomic_val = " << atomic_val.load() << std::endl;
// 链式赋值(仅语法支持,实际为多次原子存储)
atomic_val = 10 = 20; // 等价于 atomic_val = 20
std::cout << "链式赋值后 atomic_val = " << atomic_val.load() << std::endl;
return 0;
}
编译命令:g++ -std=c++11 atomic_assign.cpp -o atomic_assign
4. 交换操作:exchange() - 原子交换值(返回旧值)
- 功能:原子性地将原子变量的值替换为新值,并返回原子变量的旧值,操作不可分割。
- 函数原型:
T exchange(T desired, std::memory_order order = std::memory_order_seq_cst); - 示例:
cpp
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<std::string*> atomic_str_ptr(nullptr);
// 线程1:设置初始值
void set_ptr() {
std::string* str = new std::string("Original Value");
// 交换:将nullptr替换为str,返回旧值(nullptr)
std::string* old_ptr = atomic_str_ptr.exchange(str, std::memory_order_seq_cst);
std::cout << "线程1 交换旧值:" << (old_ptr ? old_ptr : nullptr) << std::endl;
}
// 线程2:替换值并释放旧内存
void replace_ptr() {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 等待线程1初始化
std::string* new_str = new std::string("New Value");
// 交换:获取旧值并替换为新值
std::string* old_ptr = atomic_str_ptr.exchange(new_str, std::memory_order_seq_cst);
std::cout << "线程2 交换旧值:" << *old_ptr << std::endl;
delete old_ptr; // 释放旧内存
}
int main() {
std::thread t1(set_ptr);
std::thread t2(replace_ptr);
t1.join();
t2.join();
// 释放最终内存
delete atomic_str_ptr.load();
return 0;
}
编译命令:g++ -std=c++11 atomic_exchange.cpp -o atomic_exchange -pthread
5. 比较并交换(CAS):compare_exchange_weak() / compare_exchange_strong()
- 功能:无锁编程的核心操作,原子性地比较原子变量的当前值与预期值:
- 若相等:将原子变量的值更新为目标值,返回
true; - 若不相等:将预期值更新为原子变量的当前值,返回
false。
- 两者区别:
compare_exchange_weak:可能出现"伪失败"(值相等但操作返回false),需配合循环使用,性能更高;compare_exchange_strong:无伪失败,值相等时必然返回true,性能略低。- 函数原型:
cpp
// 弱比较交换
bool compare_exchange_weak(T& expected, T desired,
std::memory_order success,
std::memory_order failure);
bool compare_exchange_weak(T& expected, T desired,
std::memory_order order = std::memory_order_seq_cst);
// 强比较交换
bool compare_exchange_strong(T& expected, T desired,
std::memory_order success,
std::memory_order failure);
bool compare_exchange_strong(T& expected, T desired,
std::memory_order order = std::memory_order_seq_cst);
- 示例(CAS实现无锁计数器):
cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0);
// 无锁计数器:每个线程累加1000次
void increment_counter() {
for (int i = 0; i < 1000; ++i) {
int expected = counter.load(std::memory_order_relaxed);
// 弱比较交换:循环处理伪失败
while (!counter.compare_exchange_weak(expected, expected + 1,
std::memory_order_relaxed,
std::memory_order_relaxed)) {
// 预期值已被自动更新为当前值,无需手动重新加载
}
}
}
int main() {
std::vector<std::thread> threads;
// 创建10个线程
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment_counter);
}
// 等待所有线程结束
for (auto& t : threads) {
t.join();
}
// 最终值应为10*1000=10000(无数据竞争,准确无误)
std::cout << "最终计数器值:" << counter.load() << std::endl;
return 0;
}
编译命令:g++ -std=c++11 atomic_cas.cpp -o atomic_cas -pthread
6. 算术原子操作:fetch_add() / fetch_sub() 等(数值类型专用)
- 功能:针对整数类型原子变量,提供原子性的算术运算,返回操作前的旧值;同时重载了
++/--运算符(简洁易用)。 - 常用函数:
fetch_add(T val):原子加法(旧值 + val,返回旧值)fetch_sub(T val):原子减法(旧值 - val,返回旧值)fetch_mul(T val):原子乘法(C++20及以上支持)- 示例:
cpp
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> atomic_num(0);
// 线程1:原子加法
void add_task() {
for (int i = 0; i < 5; ++i) {
// fetch_add返回旧值,然后原子变量+1
int old_val = atomic_num.fetch_add(1, std::memory_order_relaxed);
std::cout << "线程1 旧值:" << old_val << ",新值:" << old_val + 1 << std::endl;
}
}
// 线程2:原子减法(重载--运算符)
void sub_task() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
for (int i = 0; i < 3; ++i) {
// 前置--:先减1,再返回新值(等价于 fetch_sub(1) + 1)
--atomic_num;
std::cout << "线程2 执行--后,当前值:" << atomic_num.load() << std::endl;
}
}
int main() {
std::thread t1(add_task);
std::thread t2(sub_task);
t1.join();
t2.join();
std::cout << "最终 atomic_num = " << atomic_num.load() << std::endl;
return 0;
}
编译命令:g++ -std=c++11 atomic_arithmetic.cpp -o atomic_arithmetic -pthread
7. 位原子操作:fetch_and() / fetch_or() / fetch_xor()(整数类型专用)
- 功能:针对整数类型原子变量,提供原子性的位运算,返回操作前的旧值。
- 常用函数:
fetch_and(T val):原子按位与(旧值 & val,返回旧值)fetch_or(T val):原子按位或(旧值 | val,返回旧值)fetch_xor(T val):原子按位异或(旧值 ^ val,返回旧值)- 示例:
cpp
#include <iostream>
#include <atomic>
int main() {
std::atomic<int> atomic_bit(0b1010); // 二进制1010 = 十进制10
// 原子按位或:0b1010 | 0b0101 = 0b1111,返回旧值10
int old_val1 = atomic_bit.fetch_or(0b0101, std::memory_order_relaxed);
std::cout << "fetch_or 旧值:" << old_val1 << ",新值:" << atomic_bit.load() << std::endl;
// 原子按位与:0b1111 & 0b1100 = 0b1100,返回旧值15
int old_val2 = atomic_bit.fetch_and(0b1100, std::memory_order_relaxed);
std::cout << "fetch_and 旧值:" << old_val2 << ",新值:" << atomic_bit.load() << std::endl;
// 原子按位异或:0b1100 ^ 0b0011 = 0b1111,返回旧值12
int old_val3 = atomic_bit.fetch_xor(0b0011, std::memory_order_relaxed);
std::cout << "fetch_xor 旧值:" << old_val3 << ",新值:" << atomic_bit.load() << std::endl;
return 0;
}
编译命令:g++ -std=c++11 atomic_bit.cpp -o atomic_bit
四、std::atomic 内存顺序参数
std::atomic的所有操作都支持内存顺序参数,用于平衡正确性 与性能,对应之前讲解的强/弱内存模型,以下是常用内存顺序的示例说明:
1. std::memory_order_seq_cst(强内存模型,默认)
- 特性:全局顺序一致,禁止所有跨线程可见的重排序,编程最简单,性能最低。
- 示例(前文强内存模型示例已覆盖,此处简化):
cpp
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> x(0), y(0);
void thread1() {
x.store(1, std::memory_order_seq_cst); // 严格先执行
y.store(1, std::memory_order_seq_cst);
}
void thread2() {
while (y.load(std::memory_order_seq_cst) != 1);
// 必然能读取到x=1
std::cout << "x = " << x.load(std::memory_order_seq_cst) << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
2. std::memory_order_relaxed(最弱内存模型)
- 特性:仅保证原子操作本身不可分割,无有序性和可见性约束,性能最高,适用于独立计数器等场景。
- 示例:
cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0);
// 独立计数器:无需有序性约束,使用relaxed
void count_task() {
for (int i = 0; i < 10000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 8; ++i) {
threads.emplace_back(count_task);
}
for (auto& t : threads) {
t.join();
}
std::cout << "最终计数器值:" << counter.load(std::memory_order_relaxed) << std::endl;
return 0;
}
3. std::memory_order_acquire / std::memory_order_release(释放-获取语义,弱内存模型)
- 特性:
release:写操作,禁止前面的操作重排到该操作之后,保证修改对后续acquire操作可见;acquire:读操作,禁止后续的操作重排到该操作之前,保证能看到对应release的修改;- 示例(线程同步):
cpp
#include <iostream>
#include <atomic>
#include <thread>
#include <string>
std::atomic<bool> flag(false);
std::string shared_data = "Init";
// 生产者:release语义
void producer() {
shared_data = "Hello Acquire/Release";
flag.store(true, std::memory_order_release);
}
// 消费者:acquire语义
void consumer() {
while (!flag.load(std::memory_order_acquire));
// 必然能读取到修改后的shared_data
std::cout << "共享数据:" << shared_data << std::endl;
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
五、std::atomic 特殊场景示例
原子指针的使用(std::atomic<T*>)
- 功能:保证指针的原子操作,常用于无锁数据结构(如无锁链表)。
- 示例:
cpp
#include <iostream>
#include <atomic>
struct Node {
int val;
Node* next;
Node(int v) : val(v), next(nullptr) {}
};
// 原子指针:指向链表头
std::atomic<Node*> head(nullptr);
// 原子添加节点到链表头部
void push_node(int val) {
Node* new_node = new Node(val);
// CAS循环:保证原子插入
Node* old_head = head.load(std::memory_order_relaxed);
do {
new_node->next = old_head;
// 弱CAS,循环处理伪失败
} while (!head.compare_exchange_weak(old_head, new_node,
std::memory_order_relaxed,
std::memory_order_relaxed));
}
int main() {
push_node(10);
push_node(20);
push_node(30);
// 遍历链表
Node* curr = head.load();
while (curr) {
std::cout << curr->val << " ";
Node* next = curr->next;
delete curr;
curr = next;
}
return 0;
}
编译命令:g++ -std=c++11 atomic_ptr_node.cpp -o atomic_ptr_node
自定义类型的原子操作(需满足平凡可复制)
- 条件:自定义类型必须满足
std::is_trivially_copyable_v<T>(无虚函数、无自定义拷贝构造/赋值、成员均为平凡类型等)。 - 示例:
cpp
#include <iostream>
#include <atomic>
#include <type_traits>
// 自定义平凡可复制类型
struct MyData {
int a;
double b;
// 无自定义拷贝构造/赋值,无虚函数
};
// 验证:MyData是否为平凡可复制类型(输出true)
static_assert(std::is_trivially_copyable_v<MyData>, "MyData must be trivially copyable");
int main() {
// 自定义类型的原子变量
std::atomic<MyData> atomic_my_data;
// 存储自定义值
MyData data1{10, 3.14};
atomic_my_data.store(data1, std::memory_order_seq_cst);
// 加载自定义值
MyData data2 = atomic_my_data.load();
std::cout << "data2.a = " << data2.a << ", data2.b = " << data2.b << std::endl;
// 交换自定义值
MyData data3{20, 6.28};
MyData old_data = atomic_my_data.exchange(data3);
std::cout << "旧值:a=" << old_data.a << ", b=" << old_data.b << std::endl;
std::cout << "新值:a=" << atomic_my_data.load().a << ", b=" << atomic_my_data.load().b << std::endl;
return 0;
}
编译命令:g++ -std=c++17 atomic_custom.cpp -o atomic_custom(std::is_trivially_copyable_v需C++17及以上)
std::shared_ptr
std::shared_ptr 是唯一可以使用原子操作的非原子数据类型。说明一下这样设计的动机。
C++委员会了解到,智能指针需要在多线程中提供最小原子性保证的必要性,所以做出了这样的设计。先来解释"最小原子性保证",也就是std::shared_ptr的控制块是线程安全的,这意味着增加和减少引用计数器的是原子操作,也就能保证资源只被销毁一次了。
std::shared_ptr的声明由Boost提供:
shared_ptr实例可以被多个线程同时"读"(仅const方式访问)。- 不同的
shared_ptr实例可以被多个线程同时"写"(通过操作符=或reset等操作访问)(即使这些实例是副本,但在底层共享引用计数)。
为了使这两个表述更清楚,举一个简单的例子。当在一个线程中复制std::shared_ptr时,一切正常。
cpp
std::shared_ptr<int> ptr = std::make_shared<int>(2011);
for (auto i = 0; i < 10; i++){
std::thread([ptr]{
std::shared_ptr<int> localPtr(ptr);
localPtr = std::make_shared<int>(2014);
}).detach();
}
先看第5行,通过对std::shared_ptr localPtr使用复制构造,只使用控制块,这是线程安全的。第6行更有趣一些,为localPtr设置了一个新的std::shared_ptr。从多线程的角度来看,这不是问题:Lambda函数(第4行)通过复制绑定ptr。因此,对localPtr的修改在副本上进行。
如果通过引用获得std::shared_ptr,情况会发生巨变。
cpp
std::shared_ptr<int> ptr = std::make_shared<int>(2011);
for (auto i = 0; i < 10; i++){
std::thread([&ptr]{
ptr = std::make_shared<int>(2014);
}).detach();
}
Lambda函数通过引用,绑定了第4行中的std::shared_ptr ptr。这意味着,赋值(第5行)可能触发底层的并发读写,所以该段程序具有未定义行为(数据竞争)。
诚然,最后一个例子并不容易实现,但在多线程环境下使用std::shared_ptr也需要特别注意。同样需要注意的是,std::shared_ptr是C++中唯一存在原子操作的非原子数据类型。
六、总结
std::atomic是C++11+的无锁原子操作模板,核心解决多线程数据竞争问题,轻量高效;- 原子变量不可拷贝/移动,初始化优先使用直接初始化(
atomic_int(10)),老旧环境用ATOMIC_VAR_INIT; - 核心操作:
store()(存)、load()(取)、exchange()(交换)、CAS(比较交换,无锁核心)、算术/位操作(数值类型专用); - 内存顺序:默认
seq_cst(强模型,简单安全)、relaxed(弱模型,高性能)、acquire/release(弱模型,安全同步); - 支持类型:基本类型、指针类型、平凡可复制自定义类型,复杂同步场景仍需
std::mutex。
6种内存模型再研究
一、内存序的分类
根据同步和顺序约束的强弱,六种内存序可分为以下几类:
- 无同步约束(最弱)
memory_order_relaxed
仅保证原子操作本身的原子性,不提供任何跨线程的同步或顺序约束。操作的执行顺序可能被编译器、CPU 重排,且不保证其他线程对操作结果的可见性。
- 获取-释放(release-acquire)语义
memory_order_consume
对"依赖于当前原子操作结果"的后续操作施加约束(仅关注数据依赖),确保这些操作能看到释放侧的写入。memory_order_acquire
对当前线程的后续所有操作(加载/存储)施加约束,确保它们能看到释放侧的所有写入。memory_order_release
对当前线程的之前所有操作(加载/存储)施加约束,确保它们的结果对获取侧可见。memory_order_acq_rel
同时具备acquire和release的特性:对当前线程,之前的操作在释放前完成,之后的操作在获取后执行。
- 顺序一致性(最强)
memory_order_seq_cst
所有线程看到的原子操作顺序完全一致(全局总序),相当于所有操作都在一个全局队列中执行,是默认内存序(若不指定)。
二、不同原子操作适用的内存模型
原子操作(如 load、store、exchange 等)需根据同步需求选择内存序:
load(读取)操作- 若需获取其他线程的写入结果:
acquire、consume、seq_cst。 - 无需同步:
relaxed。
- 若需获取其他线程的写入结果:
store(写入)操作- 若需确保当前线程的写入对其他线程可见:
release、seq_cst。 - 无需同步:
relaxed。
- 若需确保当前线程的写入对其他线程可见:
exchange、compare_exchange** 等读写结合操作**- 若需同时保证读取的获取和写入的释放:
acq_rel、seq_cst。 - 无需同步:
relaxed。
- 若需同时保证读取的获取和写入的释放:
- 特殊场景
consume仅用于依赖链同步(如指针传递),适用场景较窄,实际中常被acquire替代(因编译器对consume的支持有限)。seq_cst因约束最强,性能开销最大,仅在需要全局顺序一致时使用(如多线程依赖同一状态变量)。
三、六种内存序定义的同步和顺序规则
memory_order_relaxed- 无同步:不保证操作在多线程间的执行顺序,也不保证结果的可见性。
- 仅保证原子操作本身的不可分割性(如不会读取到部分写入的值)。
memory_order_consume- 同步:若线程 A 以
release写入原子变量x,线程 B 以consume读取x,则线程 B 中"依赖于x的操作"(如通过x访问的对象)能看到线程 A 中与x相关的写入。 - 顺序:仅约束依赖链上的操作,非依赖操作可被重排。
- 同步:若线程 A 以
memory_order_acquire- 同步:与
release配对,线程 B 以acquire读取原子变量x后,能看到线程 A 以release写入x之前的所有操作结果。 - 顺序:线程 B 中
acquire后的所有操作不得被重排到acquire之前。
- 同步:与
memory_order_release- 同步:与
acquire/consume配对,线程 A 以release写入原子变量x后,其之前的所有操作结果对线程 B 可见(线程 B 以acquire/consume读取x)。 - 顺序:线程 A 中
release之前的所有操作不得被重排到release之后。
- 同步:与
memory_order_acq_rel- 同步:同时具备
acquire(读取时)和release(写入时)的同步效果。例如,原子变量x被线程 A 以release写入,线程 B 以acq_rel读写x,则 B 能看到 A 的操作,且 B 的写入也能被后续acquire线程看到。 - 顺序:约束当前线程中操作的重排(前后操作分别受
release和acquire限制)。
- 同步:同时具备
memory_order_seq_cst- 同步:所有
seq_cst操作形成全局一致的总序,任意两个seq_cst操作在所有线程中观察到的顺序相同。 - 顺序:禁止所有可能破坏全局顺序的重排,且
seq_cst加载隐含acquire,seq_cst存储隐含release,读写操作隐含acq_rel。
- 同步:所有
示例说明
1、memory_order_relaxed(最弱内存序,仅保证原子性,无同步/顺序约束)
代码示例
cpp
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
const int LOOP_COUNT = 1000000;
// 线程1:累加计数器
void thread_func1() {
for (int i = 0; i < LOOP_COUNT; ++i) {
// 仅保证counter自增操作本身是原子的,无其他约束
counter.fetch_add(1, std::memory_order_relaxed);
}
}
// 线程2:累加计数器
void thread_func2() {
for (int i = 0; i < LOOP_COUNT; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread t1(thread_func1);
std::thread t2(thread_func2);
t1.join();
t2.join();
// 最终结果一定是2000000(原子性保证),但中间过程无顺序约束
std::cout << "最终计数器值:" << counter << std::endl;
return 0;
}
代码解析
- 核心特性 :
memory_order_relaxed仅保证fetch_add(原子自增)操作不可分割,不会出现"读取-修改-写入"的中间态被其他线程打断,因此最终计数器必然是2 * LOOP_COUNT。 - 无约束体现:编译器/CPU 可重排该原子操作与线程内其他非原子操作的顺序(本示例无其他操作,仅体现原子性),且不保证线程间操作的可见性顺序(但最终原子操作的结果会一致)。
- 适用场景:独立计数器、统计事件发生次数等无需传递数据可见性的场景。
2、memory_order_consume(数据依赖同步,仅约束依赖链操作)
代码示例
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <string>
// 原子指针,用于传递数据地址
std::atomic<std::string*> ptr(nullptr);
// 非原子数据,需通过指针传递可见性
std::string data = "初始数据";
// 生产者线程:写入数据并发布指针
void producer() {
data = "生产者写入的数据"; // 先修改非原子数据
// release语义:保证之前的data修改对消费者可见
ptr.store(new std::string(data), std::memory_order_release);
}
// 消费者线程:通过consume读取指针,依赖指针访问数据
void consumer() {
std::string* local_ptr = nullptr;
// consume语义:仅保证依赖于ptr的操作能看到生产者的写入
while (!(local_ptr = ptr.load(std::memory_order_consume))) {
// 自旋等待指针发布
}
// 此处local_ptr依赖于ptr的加载结果,因此能看到data的修改
std::cout << "消费者读取到的数据:" << *local_ptr << std::endl;
delete local_ptr;
}
int main() {
std::thread t_prod(producer);
std::thread t_cons(consumer);
t_prod.join();
t_cons.join();
return 0;
}
代码解析
- 核心特性 :
memory_order_consume仅对"依赖于原子操作结果"的操作提供同步保证(本示例中local_ptr依赖ptr的加载结果,因此通过local_ptr能看到生产者的data修改)。 - 适用场景 :指针传递、数据依赖链场景(实际开发中因编译器对
consume支持有限,常被acquire替代)。 - 与acquire的区别 :
consume仅约束依赖操作,非依赖操作可被重排;acquire约束所有后续操作。
3、memory_order_acquire + memory_order_release(配对使用,传递全局可见性)
这两个内存序通常成对出现,因此合并为一个示例演示
代码示例
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<bool> ready(false); // 原子标志位,用于同步
std::vector<int> shared_data; // 非原子共享数据
// 生产者线程:填充数据并发布就绪标志
void producer() {
// 先修改非原子共享数据(无原子性要求)
shared_data.push_back(10);
shared_data.push_back(20);
shared_data.push_back(30);
// release语义:
// 1. 保证之前的shared_data修改全部完成,不会被重排到store之后
// 2. 保证这些修改对后续acquire加载ready的线程可见
ready.store(true, std::memory_order_release);
}
// 消费者线程:等待就绪标志,再读取共享数据
void consumer() {
// acquire语义:
// 1. 保证后续操作不会被重排到load之前
// 2. 一旦读取到ready为true,就能看到生产者release之前的所有修改
while (!ready.load(std::memory_order_acquire)) {
// 自旋等待就绪
}
// 安全读取共享数据
std::cout << "消费者读取共享数据:";
for (int val : shared_data) {
std::cout << val << " ";
}
std::cout << std::endl;
}
int main() {
std::thread t_prod(producer);
std::thread t_cons(consumer);
t_prod.join();
t_cons.join();
return 0;
}
代码解析
- 配对核心 :
release(生产者)与acquire(消费者)配对,形成"同步-先行"(synchronizes-with)关系,保证生产者在release之前的所有操作,对消费者acquire之后的操作可见。 - 顺序约束 :
- 生产者:
shared_data的修改不会被重排到ready.store(...)之后。 - 消费者:
shared_data的读取不会被重排到ready.load(...)之前。
- 生产者:
- 适用场景:生产者-消费者模型、消息队列、数据传递等需要保证"先写后读"可见性的场景。
4、memory_order_acq_rel(同时具备acquire和release特性,适用于读写原子操作)
代码示例
cpp
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> flag(0);
const int TARGET = 3;
// 线程1:修改flag(release)
void thread1() {
flag.store(1, std::memory_order_release);
std::cout << "线程1:将flag设为1" << std::endl;
}
// 线程2:读写flag(acq_rel:读取时acquire,写入时release)
void thread2() {
int expected = 1;
// 比较交换操作:读写结合,使用acq_rel
while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
expected = 1; // 重置预期值
}
std::cout << "线程2:读取到flag=" << expected << ",并设为2" << std::endl;
}
// 线程3:读取flag(acquire)
void thread3() {
while (flag.load(std::memory_order_acquire) != 2) {
// 自旋等待flag变为2
}
flag.store(3, std::memory_order_release);
std::cout << "线程3:读取到flag=2,并设为3" << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
std::thread t3(thread3);
t1.join();
t2.join();
t3.join();
std::cout << "最终flag值:" << flag << std::endl;
return 0;
}
代码解析
- 核心特性 :
memory_order_acq_rel用于读写结合的原子操作 (如compare_exchange_strong、fetch_add等),同时具备:acquire语义(读取原子变量时):保证线程2读取到flag=1后,能看到线程1release之前的所有操作。release语义(写入原子变量时):保证线程2写入flag=2后,其之前的操作对线程3acquire读取可见。
- 执行顺序 :线程1 → 线程2 → 线程3 依次执行,通过
acq_rel实现原子操作的读写同步。 - 适用场景:自旋锁、原子变量的链式更新、需要同时读取和同步写入的场景。
5、memory_order_seq_cst(最强内存序,全局顺序一致性)
代码示例
cpp
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> a(0);
std::atomic<int> b(0);
std::vector<std::pair<int, int>> results; // 存储线程3的读取结果
// 线程1:修改a
void thread1() {
// seq_cst:隐含release语义,且参与全局总序
a.store(1, std::memory_order_seq_cst);
}
// 线程2:修改b
void thread2() {
// seq_cst:隐含release语义,且参与全局总序
b.store(1, std::memory_order_seq_cst);
}
// 线程3:读取a和b的值
void thread3() {
int val_a = a.load(std::memory_order_seq_cst); // 隐含acquire语义
int val_b = b.load(std::memory_order_seq_cst); // 隐含acquire语义
results.emplace_back(val_a, val_b);
}
int main() {
// 多次运行,验证全局顺序一致性
for (int i = 0; i < 1000; ++i) {
a.store(0, std::memory_order_seq_cst);
b.store(0, std::memory_order_seq_cst);
results.clear();
std::thread t1(thread1);
std::thread t2(thread2);
std::thread t3(thread3);
t1.join();
t2.join();
t3.join();
// 输出读取结果(seq_cst保证不会出现(val_a=0, val_b=1)和(val_a=1, val_b=0)同时出现的矛盾)
auto [va, vb] = results[0];
std::cout << "第" << i+1 << "次:a=" << va << ",b=" << vb << std::endl;
}
return 0;
}
代码解析
- 核心特性 :
memory_order_seq_cst是默认内存序,所有seq_cst操作形成全局一致的总序 ,任意线程观察到的seq_cst操作顺序完全相同。 - 隐含语义 :
seq_cst存储 → 隐含release语义。seq_cst加载 → 隐含acquire语义。seq_cst读写操作 → 隐含acq_rel语义。
- 关键保证 :本示例中,线程1和线程2的
store操作是seq_cst,线程3的load操作也是seq_cst,因此不会出现"线程3读取到a=0,b=1,而另一个线程读取到a=1,b=0"的矛盾情况,保证全局顺序一致性。 - 适用场景:对一致性要求极高的场景(如全局状态机、分布式一致性校验),但性能开销最大。
总结
- 弱内存序(
relaxed、consume):性能高,约束少,适用于无数据依赖的场景。 - 中等内存序(
acquire、release、acq_rel):平衡性能与同步需求,是生产环境的主流选择(如生产者-消费者模型)。 - 强内存序(
seq_cst):一致性最强,性能开销最大,仅在必要时使用。 - 所有示例均可直接编译运行(需支持C++11及以上标准,编译命令:
g++ -std=c++11 文件名.cpp -pthread)。
内存序的核心是平衡同步需求与性能:弱内存序(如 relaxed)性能高但无同步,适用于独立计数器等场景;acquire/release 适用于生产者-消费者等需要传递数据可见性的场景;seq_cst 因全局顺序性,适用于对一致性要求极高的场景(但需付出性能代价)。
栅栏类型与原子类型
你希望全面理解栅栏类型、其与原子类型的区别、线程间使用方式及底层原理,同时需要代码验证每个知识点。下面将从概念厘清 、核心区别 、线程间使用 、底层原理 、代码示例五个维度展开详细说明。
一、核心概念厘清
首先需要明确:栅栏类型有双重核心含义(易混淆,需重点区分),而原子类型是数据操作层面的同步原语,三者的核心定义如下:
1. 栅栏类型的两种核心形态
| 栅栏类型 | 英文名称 | 核心定义 |
|---|---|---|
| 内存栅栏(内存屏障) | Memory Barrier/Fence | 内存模型层面的同步原语,无数据操作,仅约束指令重排序和内存缓存可见性 |
| 线程栅栏(同步栅栏) | Thread Barrier | 线程执行流层面的同步原语,阻塞线程直到一组线程全部汇合,再统一继续执行 |
2. 原子类型(Atomic Type)
- 英文:Atomic Type
- 核心定义:数据操作层面的同步原语,保证单个共享变量的操作"不可中断"(要么完全执行,要么完全不执行),解决多线程竞争修改共享变量的原子性问题,多数实现为无锁机制。
二、 栅栏类型与原子类型的核心区别(分维度对比)
为更清晰区分三者,以下从核心目标、解决问题等关键维度做对比:
| 对比维度 | 栅栏类型(内存栅栏/线程栅栏) | 原子类型 |
|---|---|---|
| 核心目标 | 1. 内存栅栏:保证内存可见性、禁止指令重排序 2. 线程栅栏:实现线程汇合等待 | 保证单个变量操作的原子性(不可中断) |
| 解决核心问题 | 1. 内存栅栏:解决"内存不可见"和"指令乱序执行"问题 2. 线程栅栏:解决"线程执行流阶段同步"问题 | 解决"多线程竞争修改共享变量"的原子性问题(避免操作被中断导致数据错乱) |
| 操作对象 | 1. 内存栅栏:操作内存访问顺序、CPU缓存 2. 线程栅栏:操作线程执行流(阻塞/唤醒) | 操作单个共享变量(原子变量实例) |
| 执行特性 | 1. 内存栅栏:无阻塞、无数据修改,仅做"约束" 2. 线程栅栏:阻塞线程(直到所有线程汇合) | 无阻塞(多数无锁实现)、操作不可中断,直接修改数据 |
| 依赖关系 | 1. 内存栅栏:可单独使用,也可被原子类型隐式嵌入(通过内存序控制) 2. 线程栅栏:底层依赖原子类型(实现计数器)+ 条件变量(实现阻塞) | 可单独使用,也可通过内存序隐式携带内存栅栏的功能 |
| 典型使用场景 | 1. 内存栅栏:配合普通共享变量实现同步(少见,多由原子类型隐式使用) 2. 线程栅栏:批量线程分阶段执行(如"数据采集阶段"完成后,统一进入"数据处理阶段") | 多线程计数(如请求数统计)、状态标记(如线程运行标识)、无锁数据结构(如无锁队列) |
三、 线程间的使用方式
1. 内存栅栏的使用方式
内存栅栏的使用分为显式使用 和隐式使用(更常用,随原子类型内存序生效):
- 显式使用:直接调用语言提供的栅栏API,手动插入内存屏障
- 隐式使用:通过原子类型的内存序(如
acquire/release/seq_cst),自动嵌入对应类型的内存栅栏,无需手动调用API - 核心操作:无数据修改,仅通过"读栅栏"(保证后续读操作从主存加载)、"写栅栏"(保证前面写操作刷入主存)、"全栅栏"(同时约束读/写)实现同步
2. 线程栅栏的使用方式
线程栅栏的使用遵循"初始化-线程汇合-后续执行"的固定流程:
- 初始化:创建线程栅栏实例,指定参与汇合的线程数量
N - 线程调用:每个线程执行完阶段性任务后,调用栅栏的"等待接口"(如
arrive_and_wait()),阻塞当前线程 - 汇合触发:当第
N个线程调用等待接口后,所有阻塞的线程被唤醒,统一继续执行后续任务 - (可选)完成回调:部分语言支持设置回调函数,当线程全部汇合后,自动执行回调逻辑
3. 原子类型的使用方式
原子类型的使用核心是"原子变量定义+原子操作调用":
- 定义原子变量:使用语言提供的原子类型模板(如C++
std::atomic<T>)包装目标类型 - 执行原子操作:调用原子变量的内置方法(如
load()读、store()写、fetch_add()自增等) - (可选)指定内存序:根据业务需求选择内存序(平衡同步安全性和性能),默认使用最严格的
seq_cst
四、 底层原理
1. 内存栅栏的底层原理
内存栅栏的核心作用是"约束"和"刷新",底层分为编译器屏障 和CPU屏障两层:
- 编译器屏障:阻止编译器在编译阶段对栅栏前后的指令进行跨栅栏重排序(仅保证编译期有序,不影响CPU执行期)
- CPU屏障:阻止CPU在执行阶段对栅栏前后的指令进行跨栅栏重排序,同时强制刷新CPU缓存:
- 写栅栏(Store Barrier):将CPU写缓冲区中的数据强制刷入主存,且保证前面的所有写操作都先于后续写操作完成
- 读栅栏(Load Barrier):使CPU失效本地缓存中对应数据,后续读操作必须从主存重新加载,且保证后面的所有读操作都晚于前面的读操作完成
- 全栅栏(Full Barrier):同时具备读栅栏和写栅栏的功能,约束所有读/写指令的顺序,且强制缓存刷新(x86架构的
lock前缀隐含全栅栏效果)
2. 线程栅栏的底层原理
线程栅栏的底层基于"原子计数器 + 条件变量"实现,核心流程:
- 初始化阶段:创建原子计数器(初始值为参与汇合的线程数
N),同时创建条件变量和互斥锁(用于线程阻塞/唤醒) - 线程等待阶段:
- 线程调用等待接口时,先加锁,然后对原子计数器执行原子减1操作
- 若计数器减1后不为0:当前线程通过条件变量阻塞,释放互斥锁,等待被唤醒
- 若计数器减1后为0:当前线程唤醒所有阻塞在该条件变量上的线程,同时重置计数器(支持循环使用,如C++20
std::barrier)
- 线程唤醒阶段:所有被阻塞的线程被唤醒后,重新获取互斥锁,然后继续执行后续逻辑
3. 原子类型的底层原理
原子类型的底层依赖CPU硬件原语 和内存序控制,核心要点:
- 硬件支撑:依赖CPU提供的原子指令集,无需内核态线程切换(无锁实现):
- x86架构:通过
lock前缀锁定内存总线/缓存行(保证多核CPU下操作的原子性),支持CAS(Compare-And-Swap)、原子自增/自减等指令 - ARM/RISC-V架构:通过LL/SC(Load-Linked/Store-Conditional)指令实现原子操作
- x86架构:通过
- 无锁特性:多数原子类型操作是无锁的(可通过
std::atomic<T>::is_lock_free()判断),仅少数复杂类型(如大结构体)会退化到基于互斥锁实现 - 内存序控制:原子操作的内存序本质是"隐式插入对应内存栅栏",不同内存序对应不同的栅栏强度:
relaxed:最弱内存序,无隐式栅栏,仅保证操作本身的原子性,不保证可见性和有序性acquire:读操作内存序,隐式插入读栅栏,保证后续读/写操作不重排到该操作之前,且能看到前面的release操作写入的数据release:写操作内存序,隐式插入写栅栏,保证前面的读/写操作不重排到该操作之后,且写入的数据对后续的acquire操作可见seq_cst:最强内存序,隐式插入全栅栏,保证所有线程看到的操作顺序一致(默认内存序,性能最差)
五、 代码示例(C++实现,逐一覆盖知识点)
以下代码均可编译运行,注释详细,对应每个核心知识点,编译命令说明:
- 原子类型/内存栅栏示例:支持C++11及以上,编译命令
g++ -std=c++11 文件名.cpp -o 输出文件 -pthread - 线程栅栏示例:支持C++20及以上,编译命令
g++ -std=c++20 文件名.cpp -o 输出文件 -pthread
示例1:原子类型的基本使用(对比非原子类型的问题)
知识点:演示非原子类型的竞争错乱问题,以及原子类型如何保证操作原子性
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
// 非原子变量:多线程竞争修改会导致数据错乱
int non_atomic_count = 0;
// 原子变量:保证自增操作不可中断
std::atomic<int> atomic_count = 0;
// 非原子变量自增函数
void non_atomic_increment(int times) {
for (int i = 0; i < times; ++i) {
// 非原子操作:读取-修改-写入 三步可被中断
non_atomic_count++;
}
}
// 原子变量自增函数
void atomic_increment(int times) {
for (int i = 0; i < times; ++i) {
// 原子自增操作:不可中断,等价于 fetch_add(1)
atomic_count++;
}
}
int main() {
const int thread_num = 10; // 线程数
const int increment_times = 100000; // 每个线程自增次数
std::vector<std::thread> threads;
// 1. 测试非原子变量
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(non_atomic_increment, increment_times);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
threads.clear();
std::cout << "理论自增结果:" << thread_num * increment_times << std::endl;
std::cout << "非原子变量实际结果:" << non_atomic_count << "(数据错乱)" << std::endl;
// 2. 测试原子变量
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(atomic_increment, increment_times);
}
for (auto& t : threads) {
t.join();
}
std::cout << "原子变量实际结果:" << atomic_count << "(数据正确)" << std::endl;
// 原子变量的其他核心操作演示
std::atomic<bool> flag = false;
flag.store(true, std::memory_order_relaxed); // 原子写操作
bool flag_val = flag.load(std::memory_order_relaxed); // 原子读操作
std::cout << "原子布尔变量值:" << std::boolalpha << flag_val << std::endl;
return 0;
}
运行结果说明:
- 非原子变量结果远小于理论值(因多线程竞争导致操作中断,数据覆盖)
- 原子变量结果与理论值完全一致(操作不可中断,保证原子性)
示例2:内存栅栏的显式使用(解决内存可见性问题)
知识点 :演示无内存栅栏时的内存不可见问题,以及std::atomic_thread_fence如何保证可见性
cpp
#include <iostream>
#include <thread>
#include <atomic>
// 普通共享变量:存在内存可见性问题
int shared_data = 0;
// 原子标志:用于控制线程退出(仅做状态标记,不传递业务数据)
std::atomic<bool> is_done = false;
// 线程1:修改共享变量,然后设置完成标志
void thread1_func() {
// 步骤1:修改普通共享变量(仅写入CPU缓存,未刷入主存)
shared_data = 100;
// 显式插入写栅栏(释放栅栏):强制将前面的写操作刷入主存,且禁止前面的指令重排到栅栏后
std::atomic_thread_fence(std::memory_order_release);
// 步骤2:设置原子标志(隐式保证可见性)
is_done.store(true, std::memory_order_relaxed);
}
// 线程2:等待完成标志,然后读取共享变量
void thread2_func() {
// 循环等待,直到原子标志为true
while (!is_done.load(std::memory_order_relaxed)) {
// 无栅栏时,可能一直读取到缓存中的false(实际已被修改)
// 此处依赖原子操作的可见性,保证能看到is_done的修改
}
// 显式插入读栅栏(获取栅栏):强制从主存加载数据,禁止后续指令重排到栅栏前
std::atomic_thread_fence(std::memory_order_acquire);
// 读取共享变量:此时能保证看到thread1修改后的100(无栅栏时可能读取到0)
std::cout << "共享变量值:" << shared_data << std::endl;
}
int main() {
std::thread t1(thread1_func);
std::thread t2(thread2_func);
t1.join();
t2.join();
return 0;
}
运行结果说明:
- 插入内存栅栏后,输出
100(保证可见性) - 若移除两个栅栏,可能输出
0(CPU缓存未刷新,导致共享变量不可见)
示例3:线程栅栏的使用(C++20 std::barrier)
知识点:演示线程栅栏如何实现多线程汇合,分阶段执行任务
cpp
#include <iostream>
#include <thread>
#include <vector>
#include <barrier> // C++20 线程栅栏头文件
// 线程栅栏完成回调函数:所有线程汇合后执行
void barrier_completion_callback() {
std::cout << "=== 所有线程已汇合,执行阶段回调 ===" << std::endl;
}
int main() {
const int thread_num = 5; // 参与汇合的线程数
// 初始化线程栅栏:指定线程数和完成回调函数
std::barrier barrier(thread_num, barrier_completion_callback);
// 线程任务函数:分两个阶段执行
auto thread_task = [&](int thread_id) {
// 阶段1:各自执行独立任务
std::cout << "线程" << thread_id << ":完成阶段1任务(如数据采集)" << std::endl;
// 调用栅栏等待接口:阻塞当前线程,直到所有线程都调用该接口
barrier.arrive_and_wait();
// 阶段2:所有线程汇合后,执行后续任务
std::cout << "线程" << thread_id << ":开始阶段2任务(如数据统一处理)" << std::endl;
};
// 创建并启动线程
std::vector<std::thread> threads;
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(thread_task, i);
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
return 0;
}
运行结果说明:
- 所有线程先输出"阶段1任务",无固定顺序(线程并行执行)
- 当5个线程全部调用
arrive_and_wait()后,执行回调函数 - 回调执行完成后,所有线程才开始输出"阶段2任务"(实现阶段同步)
示例4:原子类型+隐式内存栅栏(内存序演示)
知识点 :演示原子类型的acquire/release内存序(隐式内存栅栏),平衡性能和同步安全性
cpp
#include <iostream>
#include <thread>
#include <atomic>
int shared_value = 0;
// 原子变量:用于传递同步信号,携带隐式内存栅栏
std::atomic<int> atomic_flag = 0;
// 线程1:写操作,使用release内存序(隐式写栅栏)
void writer_thread() {
shared_value = 2024; // 普通变量写操作
// release内存序:保证前面的shared_value写操作先于该原子操作完成,且刷入主存
atomic_flag.store(1, std::memory_order_release);
}
// 线程2:读操作,使用acquire内存序(隐式读栅栏)
void reader_thread() {
// acquire内存序:保证后续的shared_value读操作晚于该原子操作完成,且从主存加载
while (atomic_flag.load(std::memory_order_acquire) != 1) {
// 循环等待,直到原子标志为1
}
// 此时能保证看到shared_value=2024(隐式内存栅栏保证可见性)
std::cout << "通过原子内存序获取的共享值:" << shared_value << std::endl;
}
int main() {
std::thread writer(writer_thread);
std::thread reader(reader_thread);
writer.join();
reader.join();
// 对比:relaxed内存序(无隐式栅栏,不保证可见性)
std::atomic<int> relaxed_flag = 0;
int relaxed_value = 0;
std::thread t1([&]() {
relaxed_value = 123;
relaxed_flag.store(1, std::memory_order_relaxed);
});
std::thread t2([&]() {
while (relaxed_flag.load(std::memory_order_relaxed) != 1);
// 此处可能读取到0(无可见性保证)
std::cout << "relaxed内存序获取的共享值:" << relaxed_value << std::endl;
});
t1.join();
t2.join();
return 0;
}
运行结果说明:
acquire/release内存序输出2024(隐式内存栅栏保证可见性)relaxed内存序可能输出0(无隐式栅栏,不保证可见性,但操作本身是原子的)acquire/release性能优于默认的seq_cst,是多线程同步的常用选择
六、 总结
- 栅栏分两类:内存栅栏(约束内存/指令,无阻塞) 、线程栅栏(线程汇合,阻塞线程);原子类型保证单个变量操作原子性(无锁为主)。
- 核心区别:栅栏侧重"顺序/流同步",原子类型侧重"数据操作不可中断";线程栅栏底层依赖原子类型,内存栅栏可被原子类型隐式使用。
- 底层支撑:内存栅栏依赖编译器/CPU屏障,线程栅栏依赖"原子计数器+条件变量",原子类型依赖CPU硬件原子指令(
lock/CAS/LL/SC)。 - 性能优先级:原子类型(
relaxed)> 原子类型(acquire/release)> 显式内存栅栏 > 原子类型(seq_cst)> 线程栅栏。 - 实用建议:优先使用原子类型(隐式内存栅栏),线程分阶段同步用线程栅栏,极少场景需要显式使用内存栅栏。