【C++】无锁原子栈:CAS实现线程安全

文章目录

无锁链表头插多线程程序完整解析

该程序实现了基于CAS的无锁单向链表头插操作 ,利用C++11的原子操作(std::atomic)和CAS(Compare-And-Swap)机制,实现多线程下的线程安全链表插入,无需互斥锁(mutex),属于典型的无锁同步编程实现。

一、核心头文件与功能说明
头文件 核心作用
<iostream> 控制台输出链表结果
<atomic> 提供原子类型和CAS核心操作
<thread> 创建和管理多线程
<vector> 存储线程对象,方便批量管理
二、关键数据结构与全局变量
  1. 链表节点结构Node :标准单向链表节点,包含int类型值value和指向下一节点的指针next,用于存储数据和维护链表结构。
  2. 原子头指针list_head
    • 类型为std::atomic<Node*>,是多线程共享的核心同步点,所有线程对链表的操作都围绕该原子指针展开;
    • 初始值为nullptr,表示链表初始为空;
    • 原子类型的核心特性:对其的读/写操作都是原子的,不会出现多线程下的"读半值/写半值"问题,保证操作的完整性。
三、核心函数:无锁头插函数append(int val)

该函数是无锁同步的核心 ,通过循环CAS(Compare-And-Swap) 实现线程安全的头插操作,整体分为3个步骤,核心解决多线程竞争下的指针同步问题:

cpp 复制代码
void append (int val) {
  Node* oldHead = list_head;          // 1. 读取当前头指针的原子值
  Node* newNode = new Node {val,oldHead}; // 2. 创建新节点,next指向当前头节点

  // 3. 循环CAS:直到原子替换头指针成功
  while (!list_head.compare_exchange_weak(oldHead,newNode))
    newNode->next = oldHead; // 失败时,更新新节点的next为最新头指针
}
步骤1:读取当前原子头指针

Node* oldHead = list_head;

原子类型std::atomic<Node*>的赋值操作是原子读 ,确保读取到的是list_head的完整、最新值,不会被其他线程的写操作打断。

步骤2:创建新节点并指向当前头

Node* newNode = new Node {val,oldHead};

新节点的next指针初始指向步骤1读取的oldHead(当前链表头),这是头插法的基础:新节点成为新头,原头变为新节点的后继。

步骤3:循环CAS实现原子替换(核心)
3.1 CAS函数:compare_exchange_weak

这是C++原子库的核心CAS操作 ,作用是原子地比较并交换,其逻辑(伪代码)为:

复制代码
bool compare_exchange_weak(T& expected, T desired) {
  原子操作:
    if (当前原子值 == expected) {
      将原子值更新为 desired;
      return true; // 交换成功
    } else {
      将 expected 更新为 当前原子值; // 关键:更新预期值
      return false; // 交换失败
    }
}
  • 入参oldHead预期值 (线程认为当前list_head应该是的值);
  • 入参newNode目标值 (线程希望将list_head更新为的值);
  • 核心特性:整个比较+交换过程是原子的,不会被任何其他线程打断,这是实现无锁同步的关键(替代互斥锁的原子性保证)。
3.2 循环的意义:处理竞争失败

while (!list_head.compare_exchange_weak(oldHead,newNode))

CAS操作可能失败 (返回false),失败的唯一原因是:当前list_head的实际值 ≠ 线程的预期值oldHead(即其他线程已经修改了头指针,发生了竞争)。

此时compare_exchange_weak会自动将oldHead更新为list_head的最新实际值 (CAS的内置行为),接着执行循环体:
newNode->next = oldHead;

将新节点的next指针重新指向最新的头指针 (因为其他线程已经修改了头,原next指向的是旧值,必须更新)。

随后进入下一次循环,用更新后的预期值oldHead 再次尝试CAS,直到交换成功(返回true)------ 这就是循环CAS(Spin CAS/自旋CAS),通过自旋重试处理多线程竞争,保证最终一定能完成插入。

3.3 为何用compare_exchange_weak而非strong

compare_exchange_weak允许伪失败 (极少数情况下,即使原子值等于预期值,也可能返回false),但性能远高于compare_exchange_strong ;结合外层while循环,伪失败会被自动重试,不影响逻辑正确性,是无锁编程的常规选择。

四、主函数main:多线程测试与链表操作

主函数完成多线程创建、等待、链表遍历、内存释放,验证无锁头插的线程安全性:

cpp 复制代码
int main ()
{
  // 1. 启动10个线程,同时插入0~9
  std::vector<std::thread> threads;
  for (int i=0; i<10; ++i) threads.push_back(std::thread(append,i));
  
  // 2. 等待所有线程执行完毕(线程汇合)
  for (auto& th : threads) th.join(); 
  
  // 3. 遍历打印链表(头插法特性:输出为插入的逆序,如9 8 7 ... 0)
  for (Node* it = list_head; it!=nullptr; it=it->next)
    std::cout << ' ' << it->value;
  std::cout << '\n';
  
  // 4. 内存释放:遍历删除所有节点(避免内存泄漏)
  Node* it; while (it=list_head) {list_head=it->next; delete it;}

  return 0;
}
关键细节说明
  1. 多线程竞争场景 :10个线程同时调用append,对同一个原子头指针list_head执行CAS操作,模拟高并发插入;
  2. 输出逆序原因 :头插法的特性------后插入的节点会成为新的链表头,因此遍历结果为9 8 7 ... 0(最后插入的9在头部,最先插入的0在尾部);
  3. 线程安全保证 :所有线程对list_head的修改都通过原子CAS完成,无数据竞争,确保链表结构始终有效(不会出现指针悬空、节点链断裂等问题);
  4. 内存释放 :遍历链表,逐个删除节点并更新头指针,避免动态分配的Node对象造成内存泄漏。
五、核心设计思想:无锁同步 vs 互斥锁同步

传统多线程链表插入需要用std::mutex加锁:

cpp 复制代码
std::mutex mtx;
void append_with_mutex(int val) {
  std::lock_guard<std::mutex> lock(mtx); // 加锁,独占访问
  Node* newNode = new Node{val, list_head};
  list_head = newNode;
}

而本程序的无锁方案通过CAS实现了更优的特性:

  1. 无阻塞(理论上) :没有互斥锁的"加锁-阻塞-解锁"过程,竞争失败的线程仅自旋重试,无需内核态切换,高并发下性能更优
  2. 原子性保证:通过CAS的原子比较+交换,替代互斥锁的独占访问,保证临界区(头指针修改)的原子性;
  3. 核心依赖:原子类型的原子读操作 + CAS的原子交换操作,共同保证多线程下的线程安全。
六、程序整体执行流程总结
  1. 初始化原子头指针list_head = nullptr,链表为空;
  2. 启动10个线程,每个线程调用append(i)(i=0~9),并发执行头插逻辑;
  3. 每个线程执行:读原子头→创建新节点→循环CAS替换头指针(竞争失败则更新新节点next并重试);
  4. 所有线程执行完毕后,链表包含10个节点,头插法形成逆序链;
  5. 遍历链表打印结果,随后释放所有节点内存,程序结束。

核心知识点提炼

  1. std::atomic<Node*>:原子指针类型,读/写操作均为原子的,是多线程共享指针的线程安全基础;
  2. CAS(Compare-And-Swap):原子的比较-交换操作,无锁同步的核心,替代互斥锁保证临界区原子性;
  3. compare_exchange_weak:CAS的具体实现,失败时自动更新预期值,结合循环处理多线程竞争;
  4. 循环CAS:无锁编程的经典模式,通过自旋重试解决CAS竞争失败问题,保证操作最终完成;
  5. 无锁头插的核心:以原子头指针为同步点,所有修改通过CAS原子完成,确保链表结构线程安全。

该程序是C++无锁同步编程的经典示例,完美体现了"原子类型+CAS"实现无锁线程安全的核心思想。

无锁原子栈(stack<T>)完整解析

该代码实现了C++泛型无锁栈 的核心push操作,基于原子指针CAS(Compare-And-Swap) 机制实现多线程安全,无需互斥锁/条件变量,属于典型的无锁同步编程,且显式指定了内存序(memory order) 优化性能,是比基础无锁链表更严谨的工业级无锁实现。

一、核心基础:类与原子头指针

cpp 复制代码
template<typename T>
class stack
{
public:
std::atomic<node<T>*> head = nullptr;
// ...push函数
};
  1. 泛型设计template<typename T> 让栈支持任意数据类型,复用性更强,是C++容器的标准设计;

  2. 原子头指针head

    • 类型为std::atomic<node<T>*>,栈的核心共享状态,所有线程的push操作都围绕该原子指针展开;
    • 初始值为nullptr,表示空栈;
    • 核心特性:对head读/写操作均为原子的,避免多线程下的"读半值/写半值",是无锁同步的基础;
  3. 节点依赖node<T> :隐含的链表节点结构(需提前定义),格式为:

    cpp 复制代码
    template<typename T>
    struct node {
        T data;        // 存储泛型数据
        node<T>* next; // 指向下一个节点的指针
    };

    栈的底层通过单向链表 实现,push操作本质是链表头插法(新节点成为新栈顶,原栈顶为新节点的后继)。

二、核心操作:push函数完整解析

push是无锁栈的核心,通过头插法+循环CAS+显式内存序实现线程安全的入栈,代码逻辑紧凑且每个步骤都有明确的设计目的:

cpp 复制代码
void push(const T& data)
{
    node<T>* new_node = new node<T>(data);  // 1. 创建新节点,存储数据
    // 2. 原子加载当前栈顶,作为新节点的后继
    new_node->next = head.load(std::memory_order_relaxed);
    // 3. 循环CAS:原子替换栈顶,处理多线程竞争
    while (!head.compare_exchange_weak(new_node->next, new_node,
                                       std::memory_order_release,
                                       std::memory_order_relaxed))
    ; // 空循环:竞争失败时自动重试
}
步骤1:创建新节点并初始化数据

node<T>* new_node = new node<T>(data);

为待入栈的数据data创建独立的链表节点,节点的next指针暂未初始化,后续将指向当前栈顶。

步骤2:原子加载当前栈顶,初始化新节点的next

new_node->next = head.load(std::memory_order_relaxed);

  1. head.load(内存序) :显式调用原子指针的原子读操作 ,读取当前head(栈顶)的实际值;
    • 区别于直接赋值(如oldHead = head),显式load可以指定内存序,更灵活地控制内存可见性和指令重排;
  2. std::memory_order_relaxed(松散内存序)
    • 这是最弱的内存序 ,仅保证当前操作本身的原子性,不限制编译器/CPU对该操作前后指令的重排,也不保证跨线程的内存可见性;
    • 此处使用的原因:仅需读取当前栈顶的瞬时值,无需与其他线程的操作建立"内存同步关系",松散内存序的性能最优(无额外的内存屏障开销)。

此步骤完成后,新节点的next指针指向执行该步骤时的栈顶节点,为后续成为新栈顶做准备。

步骤3:循环CAS实现原子替换栈顶(核心)

这是无锁push的核心逻辑,通过带内存序的CAS实现原子的栈顶替换,结合循环处理多线程竞争,空循环体的设计是该实现的典型特征。

3.1 CAS函数:compare_exchange_weak四参数版本

基础版CAS是两参数(仅传预期值、目标值),此处为四参数重载版,是无锁编程的标准用法,完整原型:

cpp 复制代码
template<class T>
bool std::atomic<T>::compare_exchange_weak(
    T& expected,                // 预期值(引用传递,会被修改)
    T desired,                  // 目标值(希望更新为的值)
    std::memory_order success, // CAS成功时的内存序
    std::memory_order failure   // CAS失败时的内存序
);

核心改进:将"成功/失败"的内存序分离,避免失败时使用强内存序带来的性能损耗,这是比基础无锁实现更严谨的地方。

3.2 本代码中CAS的参数含义
cpp 复制代码
head.compare_exchange_weak(
    new_node->next,  // 预期值(expected):当前认为的栈顶值
    new_node,        // 目标值(desired):希望成为新栈顶的新节点
    std::memory_order_release,  // 成功内存序
    std::memory_order_relaxed   // 失败内存序
)

逐一解析关键参数:

(1)预期值:new_node->next(而非单独的临时变量)

这是该实现的经典巧思 :直接将新节点的next指针作为CAS的预期值,而非额外定义old_head等临时变量。

  • 初始时,new_node->next是步骤2读取的当前栈顶(线程的预期栈顶值);
  • 若CAS失败,compare_exchange_weak自动将new_node->next更新为head的最新实际值(CAS的内置行为:预期值引用会被覆盖);
  • 此设计的好处:省去了"失败后手动更新预期值→再更新新节点next"的步骤,让代码更紧凑(空循环体即可实现重试)。
(2)目标值:new_node

线程希望将栈顶head原子更新为新节点 ,完成头插法:新节点成为新栈顶,原栈顶(存储在new_node->next)成为新节点的后继。

(3)成功内存序:std::memory_order_release(释放序)

当CAS成功 (返回true,表示当前线程成功将新节点设为栈顶)时,使用释放序,核心作用:

  1. 保证内存可见性 :当前线程对new_node的所有写操作(如初始化节点数据、设置next),对其他后续读取该栈顶的线程可见(即"释放-获取"同步);
  2. 禁止指令重排 :禁止编译器/CPU将释放序操作之前 的指令(如节点创建、next赋值)重排到该操作之后,确保节点完全初始化后才成为栈顶;
  3. 核心意义:避免其他线程读取新栈顶时,看到未初始化的节点数据(数据竞争的关键隐患)。
(4)失败内存序:std::memory_order_relaxed(松散序)

当CAS失败 (返回false,表示其他线程已修改栈顶,发生竞争)时,使用松散序,核心原因:

  • CAS失败时,当前线程未对共享变量head做任何修改 ,仅需更新预期值(new_node->next)并重试;
  • 无需建立任何跨线程的内存同步关系,使用松散序可避免不必要的内存屏障开销,最大化性能。
3.3 循环的意义:处理多线程竞争(空循环体的合理性)
cpp 复制代码
while (!CAS操作) ; // 空循环(自旋重试)
  1. CAS失败的唯一原因 :多线程竞争------当前head的实际值 ≠ 预期值new_node->next(其他线程已抢先修改栈顶);
  2. 失败后的自动处理 :CAS失败时,compare_exchange_weak会自动完成两件事:
    • new_node->next更新为head的最新实际值(预期值引用被覆盖);
    • 返回false,进入下一次循环;
  3. 空循环体的合理性 :无需额外代码,因为新节点的next已经被CAS自动更新为最新栈顶,下一次循环会直接用更新后的预期值重新尝试CAS,直到成功;
  4. 循环的本质自旋CAS(Spin CAS),竞争失败的线程不阻塞、不挂起,仅通过循环重试完成操作,这是无锁编程的典型特征(高并发下性能优于互斥锁的阻塞机制)。

三、compare_exchange_weak的选择原因

代码中使用compare_exchange_weak而非compare_exchange_strong,是无锁编程的常规优化选择,原因:

  1. weak版本允许"伪失败" :极少数情况下,即使head的实际值等于预期值,也可能返回false(与CPU架构、指令重排相关);
  2. weak版本性能更优 :避免了strong版本为了防止伪失败而做的额外检查,指令开销更小
  3. 循环抵消伪失败影响 :结合外层while循环,伪失败会被自动重试,完全不影响逻辑正确性;
  4. 适用场景 :所有需要循环CAS 的场景(如本代码的无锁栈push),weak版本都是最优选择。

四、无锁栈push的核心设计思想

  1. 底层结构 :基于单向链表头插法,栈顶即链表头,入栈操作仅需修改头指针,操作简单且高效;
  2. 同步核心 :以原子指针std::atomic<node<T>*> 作为共享状态,替代互斥锁保证操作的原子性;
  3. 竞争处理 :通过循环CAS处理多线程竞争,竞争失败的线程自旋重试,无阻塞、无内核态切换;
  4. 性能优化 :显式指定细粒度内存序 (成功release/失败relaxed),在保证线程安全的前提下,最小化内存屏障开销;
  5. 代码紧凑 :将新节点的next指针作为CAS预期值,省去临时变量,实现空循环体的简洁设计。

五、完整执行流程(单线程+多线程场景)

单线程场景(无竞争)
  1. 创建新节点,存储数据data
  2. 原子加载headnullptr),赋值给new_node->next
  3. 执行CAS:head实际值(nullptr)= 预期值(nullptr),交换成功;
  4. head更新为new_nodepush完成,栈顶为新节点。
多线程场景(有竞争,线程A/线程B同时push
  1. 线程A、B均创建新节点,均加载到当前栈顶old_head,并将new_node->next设为old_head
  2. 线程A抢先执行CAS,成功将head更新为自身的新节点,push完成;
  3. 线程B执行CAS:head实际值(线程A的新节点)≠ 预期值(old_head),CAS失败;
  4. CAS自动将线程B的new_node->next更新为head最新值(线程A的新节点);
  5. 线程B进入下一次循环,重新执行CAS;
  6. 此时无其他竞争,CAS成功,线程B的新节点成为新栈顶,push完成;
  7. 最终栈结构:线程B节点(栈顶)→ 线程A节点 → 原old_head → ... → nullptr

核心知识点提炼

  1. 泛型原子栈template<typename T>+std::atomic<node<T>*>实现通用的无锁栈结构,适配任意数据类型;
  2. 显式原子操作load()/compare_exchange_weak()是原子指针的核心显式操作,支持自定义内存序;
  3. 细粒度内存序 :成功std::memory_order_release(保证可见性/禁止重排)、失败std::memory_order_relaxed(性能最优);
  4. CAS巧思 :将新节点next作为CAS预期值,实现失败后自动更新后继,简化代码;
  5. 自旋CAS :空循环+compare_exchange_weak,处理多线程竞争,是无锁编程的经典模式;
  6. 无锁优势:无互斥锁的阻塞/解锁开销,高并发下的吞吐量和响应性优于有锁实现。

该代码是C++无锁同步编程的经典工业级实现,完美结合了泛型编程、原子操作、CAS机制和内存序优化,是理解无锁容器设计的核心范例。

相关推荐
写代码的【黑咖啡】2 小时前
Python 中的自然语言处理工具:spaCy
开发语言·python·自然语言处理
沐知全栈开发2 小时前
WSDL 语法详解
开发语言
蒹葭玉树2 小时前
【C++上岸】C++常见面试题目--操作系统篇(第三十期)
c++·面试·risc-v
wangmengxxw2 小时前
设计模式 -详解
开发语言·javascript·设计模式
froginwe112 小时前
ECharts 样式设置
开发语言
清风~徐~来2 小时前
【视频点播系统】AMQP-SDK 介绍及使用
开发语言
进击的小头2 小时前
设计模式落地的避坑指南(C语言版)
c语言·开发语言·设计模式
凤年徐2 小时前
容器适配器深度解析:从STL的stack、queue到优先队列的底层实现
开发语言·c++·算法
ujainu2 小时前
Flutter + OpenHarmony 游戏开发进阶:虚拟摄像机系统——平滑跟随与坐标偏移
开发语言·flutter·游戏·swift·openharmony