文章目录
- 自旋锁:概念、应用场景与C++11三种实现(CAS/exchange/atomic_flag)
-
- 一、自旋锁核心概念
-
- [1. 核心工作原理](#1. 核心工作原理)
- [2. 自旋锁 vs 互斥锁(std::mutex)核心差异](#2. 自旋锁 vs 互斥锁(std::mutex)核心差异)
- 二、自旋锁的应用场景
- 三、C++11三种自旋锁实现(含RAII+测试)
- 四、三种自旋锁实现对比与选型建议
- 五、自旋锁与原子操作、互斥锁的选型优先级
- 六、核心总结
自旋锁:概念、应用场景与C++11三种实现(CAS/exchange/atomic_flag)
本文整合自旋锁核心概念、适用场景,并完整实现C++11下CAS版 、std::atomic::exchange版 、std::atomic_flag版三种自旋锁,包含RAII封装、线程安全测试及选型建议,内容统一且逻辑连贯。
一、自旋锁核心概念
自旋锁是轻量级用户态同步锁机制 ,核心特征是竞争时不阻塞线程,通过循环自旋重试获取锁,直到成功拿到锁为止,是原子操作与互斥锁之间的中间同步方案,依赖CPU硬件级原子操作实现,无内核态切换开销。
1. 核心工作原理
- 用原子变量维护锁状态(
false=空闲,true=占用); - 线程尝试加锁时,通过原子操作检测锁状态:
- 锁空闲:原子性将锁置为占用状态,加锁成功进入临界区;
- 锁被占用:不挂起线程,在用户态循环(自旋)重试,直到锁释放;
- 线程解锁时,仅需原子性将锁状态置为空闲,操作轻量无开销。
2. 自旋锁 vs 互斥锁(std::mutex)核心差异
| 特性 | 自旋锁 | 互斥锁(std::mutex) |
|---|---|---|
| 竞争处理方式 | 自旋重试(忙等,不阻塞线程) | 线程阻塞(挂起,让出CPU资源) |
| 底层实现 | 硬件级原子操作(用户态同步) | 操作系统内核原语(内核态同步) |
| 核心开销 | 自旋时CPU占用(无内核切换) | 加解锁、内核/用户态切换、阻塞/唤醒开销 |
| 锁持有时间要求 | 极短(微秒级,数条CPU指令) | 无要求(支持长时持有) |
| 适用竞争程度 | 低竞争/轻竞争 | 高竞争/任意竞争 |
| 资源占用 | 竞争时CPU空转(忙等) | 竞争时让出CPU(无空转) |
简单来说:自旋锁是用户态忙等,互斥锁是内核态阻塞。
二、自旋锁的应用场景
自旋锁的优势是加解锁速度极快、无内核切换开销 ,劣势是竞争时CPU空转,因此仅适用于**「锁持有时间极短」+「低竞争」+「CPU核心充足」+「禁止阻塞」** 的场景,此时自旋的CPU开销远小于互斥锁的内核切换开销。
典型适用场景
- 临界区极短:保护单一共享变量的读-改-写、简单指针操作(仅数条CPU指令);
- 低竞争场景:多线程对临界区访问频率低,自旋几次即可获取锁;
- 禁止阻塞场景:实时系统/嵌入式系统(阻塞会降低实时性)、内核态编程(内核线程阻塞代价极高);
- 高并发低延迟要求:网络通信框架核心数据结构、无锁容器辅助同步(要求加解锁延迟最小);
- 多核环境:CPU核心充足,自旋线程占用的核心不会影响持有锁线程的执行。
绝对禁用场景
- 临界区执行时间长(含循环、IO、函数调用,毫秒级及以上);
- 高竞争场景(多个线程持续竞争,导致CPU核心被占满);
- 单CPU核心场景(自旋线程占用唯一核心,持有锁线程无法调度,引发死锁);
- 需要条件等待/唤醒的场景(如生产者-消费者,自旋锁无阻塞/唤醒机制)。
三、C++11三种自旋锁实现(含RAII+测试)
C++11标准库未直接提供自旋锁,但可通过原子操作实现三种线程安全的自旋锁,均遵循禁用拷贝移动 、合理内存序 、RAII封装原则,保证异常安全和使用便捷性。
通用RAII封装(适配所有自旋锁)
实现通用的RAII锁守卫(类似std::lock_guard),构造自动加锁、析构自动解锁 ,避免手动调用lock()/unlock()导致的漏解锁,保证异常安全,所有自旋锁均可复用该封装:
cpp
template <typename Lock>
class SpinLockGuard {
private:
Lock& m_lock; // 引用关联自旋锁,避免拷贝
public:
explicit SpinLockGuard(Lock& lock) : m_lock(lock) {
m_lock.lock(); // 构造时加锁
}
~SpinLockGuard() {
m_lock.unlock(); // 析构时解锁,异常也会执行
}
// 禁用拷贝和移动(锁是独占资源)
SpinLockGuard(const SpinLockGuard&) = delete;
SpinLockGuard& operator=(const SpinLockGuard&) = delete;
};
实现1:基于std::atomic+CAS(compare_exchange_weak)
实现原理
- 用
std::atomic<bool> m_flag{false}维护锁状态,false空闲、true占用; - 加锁:循环CAS自旋,原子性将
flag从false置为true,成功则加锁; - 解锁:原子性将
flag置为false,轻量无开销; - 可选
try_lock():单次CAS尝试加锁,失败立即返回(不自旋)。
完整代码
cpp
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
// 通用RAII封装
template <typename Lock>
class SpinLockGuard {
private:
Lock& m_lock;
public:
explicit SpinLockGuard(Lock& lock) : m_lock(lock) {
m_lock.lock();
}
~SpinLockGuard() {
m_lock.unlock();
}
SpinLockGuard(const SpinLockGuard&) = delete;
SpinLockGuard& operator=(const SpinLockGuard&) = delete;
};
// 基于CAS实现自旋锁
class SpinLockByCAS {
private:
std::atomic<bool> m_lock_flag{false};
public:
// 加锁:循环CAS自旋
void lock() {
bool expected = false;
// 使用 acquire 语义:确保获取锁后能看到之前持有锁的线程所做的修改
while (!m_lock_flag.compare_exchange_weak(
expected, true,
std::memory_order_acquire, // 成功时的内存序
std::memory_order_relaxed)) { // 失败时的内存序
expected = false; // 重置预期值
// 可选:添加主动让出或休眠,避免过度消耗CPU
// std::this_thread::yield();
}
}
// 解锁:使用 release 语义
void unlock() {
// 保证所有临界区内的操作在解锁前完成
m_lock_flag.store(false, std::memory_order_release);
}
// 尝试加锁
bool try_lock() {
bool expected = false;
// 单次尝试也需要acquire语义
return m_lock_flag.compare_exchange_weak(
expected, true,
std::memory_order_acquire,
std::memory_order_relaxed);
}
// 禁用拷贝和移动
SpinLockByCAS(const SpinLockByCAS&) = delete;
SpinLockByCAS& operator=(const SpinLockByCAS&) = delete;
SpinLockByCAS(SpinLockByCAS&&) = delete;
SpinLockByCAS& operator=(SpinLockByCAS&&) = delete;
SpinLockByCAS() = default;
~SpinLockByCAS() = default;
};
实现2:基于std::atomic::exchange(原子交换)
实现原理
- 用
std::atomic<bool> m_flag{false}维护锁状态; - 加锁:循环调用
exchange(true),原子性将锁置为占用并返回旧值,旧值为false则加锁成功; - 解锁:原子性
store(false)释放锁; - 核心:
exchange是"读旧值+写新值"的原子操作,语义更贴合自旋锁,代码比CAS版更简洁。
完整代码
cpp
// 基于std::atomic::exchange实现自旋锁
class SpinLockByExchange {
private:
std::atomic<bool> m_flag{false}; // false=空闲,true=占用
public:
// 加锁:循环exchange,返回false则加锁成功
void lock() {
// 原子交换置为true,返回旧值;acquire保证临界区内存可见性
while (m_flag.exchange(true, std::memory_order_acquire)) {
// 空循环自旋
}
}
// 解锁:原子置为false,release保证临界区修改对其他线程可见
void unlock() {
m_flag.store(false, std::memory_order_release);
}
// 尝试加锁:单次exchange,非阻塞
bool try_lock() {
return !m_flag.exchange(true, std::memory_order_acquire);
}
// 禁用拷贝和移动
SpinLockByExchange(const SpinLockByExchange&) = delete;
SpinLockByExchange& operator=(const SpinLockByExchange&) = delete;
SpinLockByExchange(SpinLockByExchange&&) = delete;
SpinLockByExchange& operator=(SpinLockByExchange&&) = delete;
SpinLockByExchange() = default;
~SpinLockByExchange() = default;
};
实现3:基于std::atomic_flag(推荐,标准无锁)
核心优势
std::atomic_flag是C++11唯一被标准强制保证无锁(lock-free) 的原子类型,仅支持核心操作(test_and_set()/clear()),操作更轻量、平台适配性更强(支持嵌入式/内核态),是实现自旋锁的工业级标准方案。
实现原理
- 用
std::atomic_flag m_flag = ATOMIC_FLAG_INIT维护锁状态(必须用该宏初始化 ,保证初始为false); - 加锁:循环调用
test_and_set(),该操作原子置为true并返回旧值 ,旧值为false则加锁成功; - 解锁:调用
clear()原子置为false,是std::atomic_flag唯一的原子清零操作。
完整代码
cpp
// 基于std::atomic_flag实现自旋锁(推荐)
class SpinLockByAtomicFlag {
private:
std::atomic_flag m_flag = ATOMIC_FLAG_INIT; // 必须用宏初始化,初始false
public:
// 加锁:循环test_and_set,返回false则加锁成功
void lock() {
// 原子置为true并返回旧值,acquire保证内存可见性
while (m_flag.test_and_set(std::memory_order_acquire)) {
// 空循环自旋
}
}
// 解锁:原子清零,release保证临界区修改对其他线程可见
void unlock() {
m_flag.clear(std::memory_order_release);
}
// 尝试加锁:单次test_and_set,非阻塞
bool try_lock() {
return !m_flag.test_and_set(std::memory_order_acquire);
}
// 禁用拷贝和移动
SpinLockByAtomicFlag(const SpinLockByAtomicFlag&) = delete;
SpinLockByAtomicFlag& operator=(const SpinLockByAtomicFlag&) = delete;
SpinLockByAtomicFlag(SpinLockByAtomicFlag&&) = delete;
SpinLockByAtomicFlag& operator=(SpinLockByAtomicFlag&&) = delete;
SpinLockByAtomicFlag() = default;
~SpinLockByAtomicFlag() = default;
};
统一线程安全测试代码
以多线程计数器为例,测试三种自旋锁的线程安全性(临界区仅1条自增指令,适配自旋锁场景),替换自旋锁类型即可测试不同实现:
cpp
// 测试用共享计数器
int g_counter = 0;
// 选择要测试的自旋锁类型(三选一)
// SpinLockByCAS g_spin_lock;
// SpinLockByExchange g_spin_lock;
SpinLockByAtomicFlag g_spin_lock;
// 线程函数:累加计数器,RAII自动加解锁
void increment(int times) {
for (int i = 0; i < times; ++i) {
SpinLockGuard<decltype(g_spin_lock)> guard(g_spin_lock);
g_counter++; // 临界区:极短,仅1条CPU指令
}
}
int main() {
const int THREAD_NUM = 10; // 10个工作线程
const int INCR_TIMES = 100000; // 每个线程累加100000次
std::vector<std::thread> threads;
// 启动多线程
for (int i = 0; i < THREAD_NUM; ++i) {
threads.emplace_back(increment, INCR_TIMES);
}
// 等待所有线程执行完毕
for (auto& t : threads) {
t.join();
}
// 正确结果:10 * 100000 = 1000000
std::cout << "自旋锁测试结果:" << g_counter << std::endl;
return 0;
}
测试结果 :三种实现均能输出正确值1000000,保证线程安全。
四、三种自旋锁实现对比与选型建议
| 特性 | CAS版(compare_exchange_weak) | exchange版 | atomic_flag版(推荐) |
|---|---|---|---|
| 标准无锁保证 | 不保证(编译器/平台相关) | 不保证 | 强制保证(lock-free) |
| 操作轻量性 | 一般 | 较轻量 | 最轻量化(仅核心操作) |
| 初始化要求 | 直接赋值false | 直接赋值false | 必须用ATOMIC_FLAG_INIT |
| 代码简洁性 | 稍复杂(需维护expected) | 简洁 | 最简洁(核心逻辑一行) |
| 平台适配性 | 通用平台(PC/服务器) | 通用平台 | 全平台(嵌入式/内核) |
| 扩展能力 | 强(易扩展各种特性) | 较强 | 基础(仅支持核心功能) |
| 内存序灵活性 | 高(成功/失败可分设内存序) | 中 | 中 |
选型原则
- 优先选atomic_flag版 :C++11实现自旋锁的标准推荐方案,强制无锁、轻量化、全平台适配,满足自旋锁所有核心场景,工业级项目首选;
- exchange版 :团队对
atomic_flag初始化规则不熟悉,或需要简单扩展时选择,语义清晰、代码简洁; - CAS版:需要高度自定义扩展(如自定义自旋策略、细粒度内存序)时选择,灵活性最高,是自旋锁的基础实现方式;
- 通用原则 :无论选择哪种实现,必须使用RAII封装,避免手动加解锁的疏漏和异常安全问题。
五、自旋锁与原子操作、互斥锁的选型优先级
自旋锁是原子操作和互斥锁的中间方案,三者需严格匹配场景,优先级从高到低为:
- 优先用原子操作 :仅保护单一共享变量的简单操作 (计数器、标志位、原子指针),直接用
std::atomic,性能最优(无锁开销); - 其次用自旋锁 :临界区是多步操作但极短(数条CPU指令)、低竞争、禁止阻塞,原子操作无法覆盖时使用(轻量、无内核切换);
- 最后用互斥锁 :临界区复杂/执行时间长 、高竞争、允许线程阻塞时,用
std::mutex(避免CPU空转,提升系统资源利用率); - 特殊场景 :需要条件等待/唤醒(如生产者-消费者),直接用
std::mutex + std::condition_variable(自旋锁无此能力)。
六、核心总结
- 自旋锁本质 :基于硬件原子操作的用户态忙等锁,竞争时自旋重试,无内核切换开销,加解锁速度极快;
- 核心适用条件 :锁持有时间极短+低竞争+CPU核心充足+禁止阻塞,缺一不可;
- C++11实现关键 :
- 用原子变量维护锁状态,保证锁操作的原子性;
- 合理选择内存序(加锁
acquire+解锁release),在保证线程安全的前提下最大化性能; - 禁用拷贝移动,避免独占资源的多实例问题;
- 必须RAII封装,保证自动加解锁和异常安全;
- 最佳实践 :优先使用
std::atomic_flag实现自旋锁,兼顾轻量化、跨平台和标准无锁保证; - 选型核心 :自旋锁是"专用轻量工具",不可替代互斥锁,也无需替代原子操作,根据临界区复杂度、竞争程度、是否允许阻塞选择最优同步方案。