C++线程之内存模型

内存模型的基础知识

内存位置是什么?

引用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、分析前提(关键!影响内存布局)

我们基于主流编译环境展开分析,避免因环境差异导致结论偏差:

  1. 架构:x86_64(64位处理器,桌面端/服务器主流架构)
  2. 编译器:GCC(g++,默认编译选项,未开启-fpack-struct(紧凑打包))
  3. 基础类型大小:char=1字节int=4字节double=8字节
  4. 对齐规则:默认自然对齐(基本类型对齐到自身大小的整数倍,结构体整体对齐到最大成员对齐数的整数倍)
  5. 位域规则:基于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)被舍弃,不占用有效位 无实际占用,仅用于位域截断 分割cd的位域单元
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、补充:不同环境的差异说明

  1. 若使用MSVC编译器:std::string大小可能为16字节(2个8字节指针),结构体总大小略有变化,但位域核心规则不变;
  2. 若开启-fpack-struct=1(紧凑打包):char a后无填充字节,a占用1字节,b直接从偏移1开始,总大小减小;
  3. 32位x86架构:std::string指针为4字节,占用12字节,结构体总大小相应减小。

总结

  1. 该结构体的内存模型受位域规则:0截断)、自然对齐 (char填充3字节、double8字节对齐)、类型大小(int4字节、double8字节)共同影响;
  2. 汇编代码通过成员偏移地址位运算指令 ,精准验证了理论布局,位域成员依赖整数单元+位运算访问,std::string区分结构体自身内存与堆内存;
  3. 核心关键:匿名位域:0强制结束位域单元、基础类型对齐要求、非基础类型(std::string)的内存分布特性。

两个线程访问相同的内存位置,会发生什么呢?

如果两个线程访问相同的内存位置(相邻位字段共享内存位置),并且至少有一个线程想要修改它,那么程序就会产生数据竞争,除非:

  1. 修改操作为原子操作。
  2. 访问按照某种先行(happens-before)顺序进行。

第二种情况非常有趣,同步语义(如互斥锁)可以建立了先行关系。这些先行关系基于原子建立,当然也适用于非原子操作。内存序(memory-ordering)是内存模型的关键部分,其定义了先行关系的细节。

强弱内存模型

你想要深入理解C++的强内存模型与弱内存模型,包括它们的核心定义、差异以及实际代码体现,我会先从理论层面清晰拆解,再通过可运行的C++代码直观演示两者的特性。

1、核心概念铺垫

C++的内存模型本质是定义多线程环境下内存操作的可见性、有序性和原子性规则,"强"与"弱"的核心区别在于对「内存操作重排序」和「跨线程可见性」的约束严格程度:

  • 重排序:编译器(编译优化)或CPU(执行优化)可能打乱代码的执行顺序(满足as-if规则:单线程内行为不变),但跨线程可能引发问题
  • 可见性:一个线程对内存的修改,能否被其他线程及时观察到

2、强内存模型(Sequential Consistency,顺序一致模型)

1. 核心定义

强内存模型是最严格的内存模型,满足两个关键特性:

  1. 单个线程内:所有内存操作严格按照代码的书写顺序执行(无指令重排,或重排对程序员完全透明)
  2. 跨线程间:所有线程看到的「全局内存操作顺序」是完全一致的,就像存在一个"全局操作队列",所有线程的内存操作都按顺序入队执行,无乱序可见性问题

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. 线程1中x.store(1)严格在y.store(1)之前执行(单线程内有序)
  2. 线程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进行更多激进优化(重排序、存储缓冲等),以提升多核架构下的性能,其核心特性:

  1. 单个线程内:满足as-if规则(看似按代码顺序执行),但实际可能发生重排序(对其他线程可见)
  2. 跨线程间:不同线程可能看到不同的「全局内存操作顺序」,无统一的全局操作队列
  3. 需显式约束:必须通过内存顺序参数 (如memory_order_relaxedmemory_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、弱内存模型代码说明

  1. 编译命令:g++ -std=c++11 -pthread weak_memory_model.cpp -o weak_model
  2. 示例1(relaxed):
    • 运行结果:在ARM、PowerPC等弱内存架构下,abnormal_count会大于0(观察到乱序);在x86_64架构下,由于硬件本身接近强内存模型,乱序概率较低,可增加测试次数(如100万次)或使用-O2优化触发编译器重排
    • 核心现象:弱模型下,线程2可能观察到y=1x=0,这是强模型下绝不会出现的情况
  3. 示例2(release/acquire):
    • 运行结果:断言永不触发,消费者必然能读取到生产者修改后的shared_data
    • 核心原理:releaseacquire形成"同步关系",既保留了弱模型的性能优势,又避免了乱序和可见性问题,是弱内存模型的主流用法

4、强内存模型 vs 弱内存模型 核心对比

对比维度 强内存模型(memory_order_seq_cst) 弱内存模型(relaxed/acquire/release)
重排序允许 禁止跨线程可见的重排序(全局有序) 允许非关键操作的重排序(宽松约束)
跨线程可见性 天然保证,无需额外约束 需显式通过内存顺序参数保证
编程复杂度 低(无需关注内存顺序,不易出错) 高(需手动选择内存顺序,易踩坑)
性能 较低(约束优化,有全局同步开销) 较高(宽松约束,充分利用硬件/编译器优化)
典型使用场景 简单同步场景(如小计数器、状态标志) 高性能要求的复杂多线程场景(如并发容器、锁实现)
对应C++内存顺序 memory_order_seq_cst(默认) memory_order_relaxed/acquire/release

总结

  1. 强内存模型(顺序一致):严格约束,编程简单,性能略低,对应memory_order_seq_cst,天然保证有序性和可见性;
  2. 弱内存模型:宽松约束,性能更高,编程复杂,需显式指定内存顺序(relaxed/acquire/release),是高性能并发编程的核心;
  3. 实际开发中:优先使用强内存模型(默认原子顺序)保证正确性,在性能瓶颈处,使用release/acquire(弱模型)优化,避免直接使用memory_order_relaxed(风险极高);
  4. 架构差异:x86_64硬件接近强内存模型,弱模型的乱序现象不明显;ARM、PowerPC等架构严格遵循弱内存模型,需重点关注内存顺序约束。

原子标志

std::atomic_flag是原子布尔类型,可以对其状态进行设置和清除。为了简化说明,我将clear状态称为false,将set状态称为trueclear方法可将其状态设置为falsetest_and_set方法,可以将状态设置回true,并返回先前的值。这里,没有方法获取当前值。使用std::atomic_flag时,必须使用常量ATOMIC_FLAG_INITstd::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();
  
}

线程tt2(第31行和第32行)在争夺临界区的访问权。这里的自旋锁是如何工作的呢?自旋锁也有锁定和解锁的阶段。

当线程t执行函数workOnResource时,可能会发生以下情况:

  1. 线程t获取锁,若第11行的标志初始值为false,则锁调用成功。这种情况下,线程t的原子操作将其设置为true。所以,当t线程获取锁后,true将会让while陷入死循环,使得线程t2陷入了激烈的竞争当中。线程t2不能将标志设置为false,因此t2必须等待,直到线程t1执行unlock(解锁)并将标志设置为false(第14 - 16行)时,才能获取锁。
  2. 线程t没有得到锁时,情况1中的t2一样,需要等待。

输出如下:

cpp 复制代码
id 129839973529152 locking ...
id 129839965136448 locking ...

我们将注意力放在std::atomic_flagtest_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()
  • 功能:无锁编程的核心操作,原子性地比较原子变量的当前值与预期值:
  1. 若相等:将原子变量的值更新为目标值,返回true
  2. 若不相等:将预期值更新为原子变量的当前值,返回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_customstd::is_trivially_copyable_v需C++17及以上)

std::shared_ptr

std::shared_ptr 是唯一可以使用原子操作的非原子数据类型。说明一下这样设计的动机。

C++委员会了解到,智能指针需要在多线程中提供最小原子性保证的必要性,所以做出了这样的设计。先来解释"最小原子性保证",也就是std::shared_ptr的控制块是线程安全的,这意味着增加和减少引用计数器的是原子操作,也就能保证资源只被销毁一次了。

std::shared_ptr的声明由Boost提供:

  1. shared_ptr实例可以被多个线程同时"读"(仅const方式访问)。
  2. 不同的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++中唯一存在原子操作的非原子数据类型。

六、总结

  1. std::atomic是C++11+的无锁原子操作模板,核心解决多线程数据竞争问题,轻量高效;
  2. 原子变量不可拷贝/移动,初始化优先使用直接初始化(atomic_int(10)),老旧环境用ATOMIC_VAR_INIT
  3. 核心操作:store()(存)、load()(取)、exchange()(交换)、CAS(比较交换,无锁核心)、算术/位操作(数值类型专用);
  4. 内存顺序:默认seq_cst(强模型,简单安全)、relaxed(弱模型,高性能)、acquire/release(弱模型,安全同步);
  5. 支持类型:基本类型、指针类型、平凡可复制自定义类型,复杂同步场景仍需std::mutex

6种内存模型再研究

一、内存序的分类

根据同步和顺序约束的强弱,六种内存序可分为以下几类:

  1. 无同步约束(最弱)
    • memory_order_relaxed
      仅保证原子操作本身的原子性,不提供任何跨线程的同步或顺序约束。操作的执行顺序可能被编译器、CPU 重排,且不保证其他线程对操作结果的可见性。
  2. 获取-释放(release-acquire)语义
    • memory_order_consume
      对"依赖于当前原子操作结果"的后续操作施加约束(仅关注数据依赖),确保这些操作能看到释放侧的写入。
    • memory_order_acquire
      对当前线程的后续所有操作(加载/存储)施加约束,确保它们能看到释放侧的所有写入。
    • memory_order_release
      对当前线程的之前所有操作(加载/存储)施加约束,确保它们的结果对获取侧可见。
    • memory_order_acq_rel
      同时具备 acquirerelease 的特性:对当前线程,之前的操作在释放前完成,之后的操作在获取后执行。
  3. 顺序一致性(最强)
    • memory_order_seq_cst
      所有线程看到的原子操作顺序完全一致(全局总序),相当于所有操作都在一个全局队列中执行,是默认内存序(若不指定)。

二、不同原子操作适用的内存模型

原子操作(如 loadstoreexchange 等)需根据同步需求选择内存序:

  1. load(读取)操作
    • 若需获取其他线程的写入结果:acquireconsumeseq_cst
    • 无需同步:relaxed
  2. store(写入)操作
    • 若需确保当前线程的写入对其他线程可见:releaseseq_cst
    • 无需同步:relaxed
  3. exchange compare_exchange** 等读写结合操作**
    • 若需同时保证读取的获取和写入的释放:acq_relseq_cst
    • 无需同步:relaxed
  4. 特殊场景
    • consume 仅用于依赖链同步(如指针传递),适用场景较窄,实际中常被 acquire 替代(因编译器对 consume 的支持有限)。
    • seq_cst 因约束最强,性能开销最大,仅在需要全局顺序一致时使用(如多线程依赖同一状态变量)。

三、六种内存序定义的同步和顺序规则

  1. memory_order_relaxed
    • 无同步:不保证操作在多线程间的执行顺序,也不保证结果的可见性。
    • 仅保证原子操作本身的不可分割性(如不会读取到部分写入的值)。
  2. memory_order_consume
    • 同步:若线程 A 以 release 写入原子变量 x,线程 B 以 consume 读取 x,则线程 B 中"依赖于 x 的操作"(如通过 x 访问的对象)能看到线程 A 中与 x 相关的写入。
    • 顺序:仅约束依赖链上的操作,非依赖操作可被重排。
  3. memory_order_acquire
    • 同步:与 release 配对,线程 B 以 acquire 读取原子变量 x 后,能看到线程 A 以 release 写入 x 之前的所有操作结果。
    • 顺序:线程 B 中 acquire 后的所有操作不得被重排到 acquire 之前。
  4. memory_order_release
    • 同步:与 acquire/consume 配对,线程 A 以 release 写入原子变量 x 后,其之前的所有操作结果对线程 B 可见(线程 B 以 acquire/consume 读取 x)。
    • 顺序:线程 A 中 release 之前的所有操作不得被重排到 release 之后。
  5. memory_order_acq_rel
    • 同步:同时具备 acquire(读取时)和 release(写入时)的同步效果。例如,原子变量 x 被线程 A 以 release 写入,线程 B 以 acq_rel 读写 x,则 B 能看到 A 的操作,且 B 的写入也能被后续 acquire 线程看到。
    • 顺序:约束当前线程中操作的重排(前后操作分别受 releaseacquire 限制)。
  6. memory_order_seq_cst
    • 同步:所有 seq_cst 操作形成全局一致的总序,任意两个 seq_cst 操作在所有线程中观察到的顺序相同。
    • 顺序:禁止所有可能破坏全局顺序的重排,且 seq_cst 加载隐含 acquireseq_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;
}
代码解析
  1. 核心特性memory_order_relaxed 仅保证 fetch_add(原子自增)操作不可分割,不会出现"读取-修改-写入"的中间态被其他线程打断,因此最终计数器必然是 2 * LOOP_COUNT
  2. 无约束体现:编译器/CPU 可重排该原子操作与线程内其他非原子操作的顺序(本示例无其他操作,仅体现原子性),且不保证线程间操作的可见性顺序(但最终原子操作的结果会一致)。
  3. 适用场景:独立计数器、统计事件发生次数等无需传递数据可见性的场景。

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;
}
代码解析
  1. 核心特性memory_order_consume 仅对"依赖于原子操作结果"的操作提供同步保证(本示例中 local_ptr 依赖 ptr 的加载结果,因此通过 local_ptr 能看到生产者的 data 修改)。
  2. 适用场景 :指针传递、数据依赖链场景(实际开发中因编译器对 consume 支持有限,常被 acquire 替代)。
  3. 与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;
}
代码解析
  1. 配对核心release(生产者)与 acquire(消费者)配对,形成"同步-先行"(synchronizes-with)关系,保证生产者在 release 之前的所有操作,对消费者 acquire 之后的操作可见。
  2. 顺序约束
    • 生产者:shared_data 的修改不会被重排到 ready.store(...) 之后。
    • 消费者:shared_data 的读取不会被重排到 ready.load(...) 之前。
  3. 适用场景:生产者-消费者模型、消息队列、数据传递等需要保证"先写后读"可见性的场景。

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;
}
代码解析
  1. 核心特性memory_order_acq_rel 用于读写结合的原子操作 (如 compare_exchange_strongfetch_add 等),同时具备:
    • acquire 语义(读取原子变量时):保证线程2读取到 flag=1 后,能看到线程1 release 之前的所有操作。
    • release 语义(写入原子变量时):保证线程2写入 flag=2 后,其之前的操作对线程3 acquire 读取可见。
  2. 执行顺序 :线程1 → 线程2 → 线程3 依次执行,通过 acq_rel 实现原子操作的读写同步。
  3. 适用场景:自旋锁、原子变量的链式更新、需要同时读取和同步写入的场景。

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;
}
代码解析
  1. 核心特性memory_order_seq_cst 是默认内存序,所有 seq_cst 操作形成全局一致的总序 ,任意线程观察到的 seq_cst 操作顺序完全相同。
  2. 隐含语义
    • seq_cst 存储 → 隐含 release 语义。
    • seq_cst 加载 → 隐含 acquire 语义。
    • seq_cst 读写操作 → 隐含 acq_rel 语义。
  3. 关键保证 :本示例中,线程1和线程2的 store 操作是 seq_cst,线程3的 load 操作也是 seq_cst,因此不会出现"线程3读取到 a=0,b=1,而另一个线程读取到 a=1,b=0"的矛盾情况,保证全局顺序一致性。
  4. 适用场景:对一致性要求极高的场景(如全局状态机、分布式一致性校验),但性能开销最大。

总结

  1. 弱内存序(relaxedconsume):性能高,约束少,适用于无数据依赖的场景。
  2. 中等内存序(acquirereleaseacq_rel):平衡性能与同步需求,是生产环境的主流选择(如生产者-消费者模型)。
  3. 强内存序(seq_cst):一致性最强,性能开销最大,仅在必要时使用。
  4. 所有示例均可直接编译运行(需支持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. 线程栅栏的使用方式

线程栅栏的使用遵循"初始化-线程汇合-后续执行"的固定流程:

  1. 初始化:创建线程栅栏实例,指定参与汇合的线程数量N
  2. 线程调用:每个线程执行完阶段性任务后,调用栅栏的"等待接口"(如arrive_and_wait()),阻塞当前线程
  3. 汇合触发:当第N个线程调用等待接口后,所有阻塞的线程被唤醒,统一继续执行后续任务
  4. (可选)完成回调:部分语言支持设置回调函数,当线程全部汇合后,自动执行回调逻辑

3. 原子类型的使用方式

原子类型的使用核心是"原子变量定义+原子操作调用":

  1. 定义原子变量:使用语言提供的原子类型模板(如C++ std::atomic<T>)包装目标类型
  2. 执行原子操作:调用原子变量的内置方法(如load()读、store()写、fetch_add()自增等)
  3. (可选)指定内存序:根据业务需求选择内存序(平衡同步安全性和性能),默认使用最严格的seq_cst

四、 底层原理

1. 内存栅栏的底层原理

内存栅栏的核心作用是"约束"和"刷新",底层分为编译器屏障CPU屏障两层:

  • 编译器屏障:阻止编译器在编译阶段对栅栏前后的指令进行跨栅栏重排序(仅保证编译期有序,不影响CPU执行期)
  • CPU屏障:阻止CPU在执行阶段对栅栏前后的指令进行跨栅栏重排序,同时强制刷新CPU缓存:
    1. 写栅栏(Store Barrier):将CPU写缓冲区中的数据强制刷入主存,且保证前面的所有写操作都先于后续写操作完成
    2. 读栅栏(Load Barrier):使CPU失效本地缓存中对应数据,后续读操作必须从主存重新加载,且保证后面的所有读操作都晚于前面的读操作完成
    3. 全栅栏(Full Barrier):同时具备读栅栏和写栅栏的功能,约束所有读/写指令的顺序,且强制缓存刷新(x86架构的lock前缀隐含全栅栏效果)

2. 线程栅栏的底层原理

线程栅栏的底层基于"原子计数器 + 条件变量"实现,核心流程:

  1. 初始化阶段:创建原子计数器(初始值为参与汇合的线程数N),同时创建条件变量和互斥锁(用于线程阻塞/唤醒)
  2. 线程等待阶段:
    • 线程调用等待接口时,先加锁,然后对原子计数器执行原子减1操作
    • 若计数器减1后不为0:当前线程通过条件变量阻塞,释放互斥锁,等待被唤醒
    • 若计数器减1后为0:当前线程唤醒所有阻塞在该条件变量上的线程,同时重置计数器(支持循环使用,如C++20 std::barrier
  3. 线程唤醒阶段:所有被阻塞的线程被唤醒后,重新获取互斥锁,然后继续执行后续逻辑

3. 原子类型的底层原理

原子类型的底层依赖CPU硬件原语内存序控制,核心要点:

  • 硬件支撑:依赖CPU提供的原子指令集,无需内核态线程切换(无锁实现):
    1. x86架构:通过lock前缀锁定内存总线/缓存行(保证多核CPU下操作的原子性),支持CAS(Compare-And-Swap)、原子自增/自减等指令
    2. ARM/RISC-V架构:通过LL/SC(Load-Linked/Store-Conditional)指令实现原子操作
  • 无锁特性:多数原子类型操作是无锁的(可通过std::atomic<T>::is_lock_free()判断),仅少数复杂类型(如大结构体)会退化到基于互斥锁实现
  • 内存序控制:原子操作的内存序本质是"隐式插入对应内存栅栏",不同内存序对应不同的栅栏强度:
    1. relaxed:最弱内存序,无隐式栅栏,仅保证操作本身的原子性,不保证可见性和有序性
    2. acquire:读操作内存序,隐式插入读栅栏,保证后续读/写操作不重排到该操作之前,且能看到前面的release操作写入的数据
    3. release:写操作内存序,隐式插入写栅栏,保证前面的读/写操作不重排到该操作之后,且写入的数据对后续的acquire操作可见
    4. 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,是多线程同步的常用选择

六、 总结

  1. 栅栏分两类:内存栅栏(约束内存/指令,无阻塞)线程栅栏(线程汇合,阻塞线程);原子类型保证单个变量操作原子性(无锁为主)。
  2. 核心区别:栅栏侧重"顺序/流同步",原子类型侧重"数据操作不可中断";线程栅栏底层依赖原子类型,内存栅栏可被原子类型隐式使用。
  3. 底层支撑:内存栅栏依赖编译器/CPU屏障,线程栅栏依赖"原子计数器+条件变量",原子类型依赖CPU硬件原子指令(lock/CAS/LL/SC)。
  4. 性能优先级:原子类型(relaxed)> 原子类型(acquire/release)> 显式内存栅栏 > 原子类型(seq_cst)> 线程栅栏。
  5. 实用建议:优先使用原子类型(隐式内存栅栏),线程分阶段同步用线程栅栏,极少场景需要显式使用内存栅栏。
相关推荐
梵尔纳多21 小时前
绘制一个三角形
c++·图形渲染·opengl
汉克老师1 天前
GESP2025年12月认证C++六级真题与解析(单选题8-15)
c++·算法·二叉树·动态规划·哈夫曼编码·gesp6级·gesp六级
郝学胜-神的一滴1 天前
线程同步:并行世界的秩序守护者
java·linux·开发语言·c++·程序人生
im_AMBER1 天前
Leetcode 95 分割链表
数据结构·c++·笔记·学习·算法·leetcode·链表
明洞日记1 天前
【VTK手册032】vtkImageConstantPad:医学图像边界填充与尺寸对齐
c++·图像处理·vtk·图形渲染
Aevget1 天前
MFC扩展库BCGControlBar Pro v37.1亮点:Ribbon Bar组件全新升级
c++·ribbon·mfc·bcg·界面控件·ui开发
cchjyq1 天前
嵌入式按键调参:简洁接口轻松调参(ADC FLASH 按键 屏幕参数显示)
c语言·c++·单片机·mcu·开源·开源软件
程序炼丹师1 天前
std::runtime_error是否会终止程序
c++
qq_433554541 天前
C++字符串hash
c++·算法·哈希算法