文章目录
-
- 无锁链表头插多线程程序完整解析
-
-
- 一、核心头文件与功能说明
- 二、关键数据结构与全局变量
- [三、核心函数:无锁头插函数`append(int val)`](#三、核心函数:无锁头插函数
append(int val)) -
- 步骤1:读取当前原子头指针
- 步骤2:创建新节点并指向当前头
- 步骤3:循环CAS实现原子替换(核心)
-
- [3.1 CAS函数:`compare_exchange_weak`](#3.1 CAS函数:
compare_exchange_weak) - [3.2 循环的意义:处理竞争失败](#3.2 循环的意义:处理竞争失败)
- [3.3 为何用`compare_exchange_weak`而非`strong`?](#3.3 为何用
compare_exchange_weak而非strong?)
- [3.1 CAS函数:`compare_exchange_weak`](#3.1 CAS函数:
- 四、主函数`main`:多线程测试与链表操作
- [五、核心设计思想:无锁同步 vs 互斥锁同步](#五、核心设计思想:无锁同步 vs 互斥锁同步)
- 六、程序整体执行流程总结
- 核心知识点提炼
-
- 无锁原子栈(`stack<T>`)完整解析
-
- 一、核心基础:类与原子头指针
- 二、核心操作:`push`函数完整解析
-
- 步骤1:创建新节点并初始化数据
- 步骤2:原子加载当前栈顶,初始化新节点的`next`
- 步骤3:循环CAS实现原子替换栈顶(核心)
-
- [3.1 CAS函数:`compare_exchange_weak`四参数版本](#3.1 CAS函数:
compare_exchange_weak四参数版本) - [3.2 本代码中CAS的参数含义](#3.2 本代码中CAS的参数含义)
- [3.3 循环的意义:处理多线程竞争(空循环体的合理性)](#3.3 循环的意义:处理多线程竞争(空循环体的合理性))
- [3.1 CAS函数:`compare_exchange_weak`四参数版本](#3.1 CAS函数:
- 三、`compare_exchange_weak`的选择原因
- 四、无锁栈`push`的核心设计思想
- 五、完整执行流程(单线程+多线程场景)
- 核心知识点提炼
无锁链表头插多线程程序完整解析
该程序实现了基于CAS的无锁单向链表头插操作 ,利用C++11的原子操作(std::atomic)和CAS(Compare-And-Swap)机制,实现多线程下的线程安全链表插入,无需互斥锁(mutex),属于典型的无锁同步编程实现。
一、核心头文件与功能说明
| 头文件 | 核心作用 |
|---|---|
<iostream> |
控制台输出链表结果 |
<atomic> |
提供原子类型和CAS核心操作 |
<thread> |
创建和管理多线程 |
<vector> |
存储线程对象,方便批量管理 |
二、关键数据结构与全局变量
- 链表节点结构
Node:标准单向链表节点,包含int类型值value和指向下一节点的指针next,用于存储数据和维护链表结构。 - 原子头指针
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;
}
关键细节说明
- 多线程竞争场景 :10个线程同时调用
append,对同一个原子头指针list_head执行CAS操作,模拟高并发插入; - 输出逆序原因 :头插法的特性------后插入的节点会成为新的链表头,因此遍历结果为
9 8 7 ... 0(最后插入的9在头部,最先插入的0在尾部); - 线程安全保证 :所有线程对
list_head的修改都通过原子CAS完成,无数据竞争,确保链表结构始终有效(不会出现指针悬空、节点链断裂等问题); - 内存释放 :遍历链表,逐个删除节点并更新头指针,避免动态分配的
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实现了更优的特性:
- 无阻塞(理论上) :没有互斥锁的"加锁-阻塞-解锁"过程,竞争失败的线程仅自旋重试,无需内核态切换,高并发下性能更优;
- 原子性保证:通过CAS的原子比较+交换,替代互斥锁的独占访问,保证临界区(头指针修改)的原子性;
- 核心依赖:原子类型的原子读操作 + CAS的原子交换操作,共同保证多线程下的线程安全。
六、程序整体执行流程总结
- 初始化原子头指针
list_head = nullptr,链表为空; - 启动10个线程,每个线程调用
append(i)(i=0~9),并发执行头插逻辑; - 每个线程执行:读原子头→创建新节点→循环CAS替换头指针(竞争失败则更新新节点next并重试);
- 所有线程执行完毕后,链表包含10个节点,头插法形成逆序链;
- 遍历链表打印结果,随后释放所有节点内存,程序结束。
核心知识点提炼
std::atomic<Node*>:原子指针类型,读/写操作均为原子的,是多线程共享指针的线程安全基础;- CAS(Compare-And-Swap):原子的比较-交换操作,无锁同步的核心,替代互斥锁保证临界区原子性;
compare_exchange_weak:CAS的具体实现,失败时自动更新预期值,结合循环处理多线程竞争;- 循环CAS:无锁编程的经典模式,通过自旋重试解决CAS竞争失败问题,保证操作最终完成;
- 无锁头插的核心:以原子头指针为同步点,所有修改通过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函数
};
-
泛型设计 :
template<typename T>让栈支持任意数据类型,复用性更强,是C++容器的标准设计; -
原子头指针
head:- 类型为
std::atomic<node<T>*>,栈的核心共享状态,所有线程的push操作都围绕该原子指针展开; - 初始值为
nullptr,表示空栈; - 核心特性:对
head的读/写操作均为原子的,避免多线程下的"读半值/写半值",是无锁同步的基础;
- 类型为
-
节点依赖
node<T>:隐含的链表节点结构(需提前定义),格式为:cpptemplate<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);
head.load(内存序):显式调用原子指针的原子读操作 ,读取当前head(栈顶)的实际值;- 区别于直接赋值(如
oldHead = head),显式load可以指定内存序,更灵活地控制内存可见性和指令重排;
- 区别于直接赋值(如
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,表示当前线程成功将新节点设为栈顶)时,使用释放序,核心作用:
- 保证内存可见性 :当前线程对
new_node的所有写操作(如初始化节点数据、设置next),对其他后续读取该栈顶的线程可见(即"释放-获取"同步); - 禁止指令重排 :禁止编译器/CPU将释放序操作之前 的指令(如节点创建、
next赋值)重排到该操作之后,确保节点完全初始化后才成为栈顶; - 核心意义:避免其他线程读取新栈顶时,看到未初始化的节点数据(数据竞争的关键隐患)。
(4)失败内存序:std::memory_order_relaxed(松散序)
当CAS失败 (返回false,表示其他线程已修改栈顶,发生竞争)时,使用松散序,核心原因:
- CAS失败时,当前线程未对共享变量
head做任何修改 ,仅需更新预期值(new_node->next)并重试; - 无需建立任何跨线程的内存同步关系,使用松散序可避免不必要的内存屏障开销,最大化性能。
3.3 循环的意义:处理多线程竞争(空循环体的合理性)
cpp
while (!CAS操作) ; // 空循环(自旋重试)
- CAS失败的唯一原因 :多线程竞争------当前
head的实际值 ≠ 预期值new_node->next(其他线程已抢先修改栈顶); - 失败后的自动处理 :CAS失败时,
compare_exchange_weak会自动完成两件事:- 将
new_node->next更新为head的最新实际值(预期值引用被覆盖); - 返回
false,进入下一次循环;
- 将
- 空循环体的合理性 :无需额外代码,因为新节点的
next已经被CAS自动更新为最新栈顶,下一次循环会直接用更新后的预期值重新尝试CAS,直到成功; - 循环的本质 :自旋CAS(Spin CAS),竞争失败的线程不阻塞、不挂起,仅通过循环重试完成操作,这是无锁编程的典型特征(高并发下性能优于互斥锁的阻塞机制)。
三、compare_exchange_weak的选择原因
代码中使用compare_exchange_weak而非compare_exchange_strong,是无锁编程的常规优化选择,原因:
weak版本允许"伪失败" :极少数情况下,即使head的实际值等于预期值,也可能返回false(与CPU架构、指令重排相关);weak版本性能更优 :避免了strong版本为了防止伪失败而做的额外检查,指令开销更小;- 循环抵消伪失败影响 :结合外层
while循环,伪失败会被自动重试,完全不影响逻辑正确性; - 适用场景 :所有需要循环CAS 的场景(如本代码的无锁栈
push),weak版本都是最优选择。
四、无锁栈push的核心设计思想
- 底层结构 :基于单向链表头插法,栈顶即链表头,入栈操作仅需修改头指针,操作简单且高效;
- 同步核心 :以原子指针
std::atomic<node<T>*>作为共享状态,替代互斥锁保证操作的原子性; - 竞争处理 :通过循环CAS处理多线程竞争,竞争失败的线程自旋重试,无阻塞、无内核态切换;
- 性能优化 :显式指定细粒度内存序 (成功
release/失败relaxed),在保证线程安全的前提下,最小化内存屏障开销; - 代码紧凑 :将新节点的
next指针作为CAS预期值,省去临时变量,实现空循环体的简洁设计。
五、完整执行流程(单线程+多线程场景)
单线程场景(无竞争)
- 创建新节点,存储数据
data; - 原子加载
head(nullptr),赋值给new_node->next; - 执行CAS:
head实际值(nullptr)= 预期值(nullptr),交换成功; head更新为new_node,push完成,栈顶为新节点。
多线程场景(有竞争,线程A/线程B同时push)
- 线程A、B均创建新节点,均加载到当前栈顶
old_head,并将new_node->next设为old_head; - 线程A抢先执行CAS,成功将
head更新为自身的新节点,push完成; - 线程B执行CAS:
head实际值(线程A的新节点)≠ 预期值(old_head),CAS失败; - CAS自动将线程B的
new_node->next更新为head最新值(线程A的新节点); - 线程B进入下一次循环,重新执行CAS;
- 此时无其他竞争,CAS成功,线程B的新节点成为新栈顶,
push完成; - 最终栈结构:线程B节点(栈顶)→ 线程A节点 → 原
old_head→ ... →nullptr。
核心知识点提炼
- 泛型原子栈 :
template<typename T>+std::atomic<node<T>*>实现通用的无锁栈结构,适配任意数据类型; - 显式原子操作 :
load()/compare_exchange_weak()是原子指针的核心显式操作,支持自定义内存序; - 细粒度内存序 :成功
std::memory_order_release(保证可见性/禁止重排)、失败std::memory_order_relaxed(性能最优); - CAS巧思 :将新节点
next作为CAS预期值,实现失败后自动更新后继,简化代码; - 自旋CAS :空循环+
compare_exchange_weak,处理多线程竞争,是无锁编程的经典模式; - 无锁优势:无互斥锁的阻塞/解锁开销,高并发下的吞吐量和响应性优于有锁实现。
该代码是C++无锁同步编程的经典工业级实现,完美结合了泛型编程、原子操作、CAS机制和内存序优化,是理解无锁容器设计的核心范例。