自旋锁是一种忙等锁,当线程尝试获取锁时如果锁已被占用,线程不会进入休眠状态,而是会不断循环检查锁的状态直到获取成功,这种特性让它在临界区执行耗时极短的场景下比互斥锁有更低的切换开销。

自旋锁的实现原理
自旋锁的核心逻辑包含两个操作:获取锁和释放锁。获取锁时尝试将锁的状态从空闲修改为占用,如果修改失败就不断重试;释放锁时将锁的状态改回空闲。要实现正确的自旋锁,这两个操作必须是原子操作,否则会出现多个线程同时获取到锁的问题。
C++11引入的std::atomic_flag是最简单的原子类型,它只有两种状态:设置态和清除态,提供的test_and_set和clear方法都是原子操作,非常适合用来实现自旋锁的底层状态管理。
基于std::atomic_flag的自旋锁实现
基础自旋锁实现
下面是一个最基础的自旋锁实现,包含构造、加锁、解锁、尝试加锁四个核心方法:
#include <atomic>
#include <thread>
class SpinLock {
private:
// atomic_flag默认初始化为清除态,即锁空闲
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
// 禁止拷贝构造和赋值,避免锁状态被错误复制
SpinLock(const SpinLock&) = delete;
SpinLock& operator=(const SpinLock&) = delete;
SpinLock() = default;
// 加锁:不断尝试将flag设置为设置态,直到成功
void lock() {
// test_and_set原子操作:返回flag之前的状态,然后将flag设为设置态
// 如果之前是清除态(返回false),说明加锁成功;否则循环重试
while (flag.test_and_set(std::memory_order_acquire)) {
// 忙等,线程会一直循环检查flag状态
}
}
// 解锁:将flag恢复为清除态
void unlock() {
// memory_order_release保证解锁前的所有操作对获取锁的线程可见
flag.clear(std::memory_order_release);
}
// 尝试加锁:如果锁空闲则获取,否则立即返回
bool try_lock() {
// 尝试设置flag,返回true说明之前是清除态,加锁成功
return !flag.test_and_set(std::memory_order_acquire);
}
};
内存序说明
上面的实现中使用了两种内存序:
- std::memory_order_acquire:用在加锁和尝试加锁的
test_and_set操作,保证当前线程后续的所有读操作不会被重排到该操作之前,同时能看到其他线程释放锁前的所有写操作。 - std::memory_order_release:用在解锁的
clear操作,保证当前线程解锁前的所有写操作不会被重排到该操作之后,同时能被后续获取锁的线程看到。
这两种内存序搭配使用,能够保证自旋锁的临界区操作的可见性,避免出现数据竞争问题。
自旋锁使用示例
下面是一个多线程使用自旋锁保护共享变量的示例:
#include <iostream>
#include <vector>
#include <thread>
// 共享变量
int shared_counter = 0;
// 定义自旋锁
SpinLock spin_lock;
// 线程执行的函数,对共享变量加1000次
void increment_task(int times) {
for (int i = 0; i < times; ++i) {
spin_lock.lock();
++shared_counter;
spin_lock.unlock();
}
}
int main() {
const int thread_num = 4;
const int increment_times = 1000;
std::vector<std::thread> threads;
// 创建4个线程
for (int i = 0; i < thread_num; ++i) {
threads.emplace_back(increment_task, increment_times);
}
// 等待所有线程执行完成
for (auto& t : threads) {
t.join();
}
// 预期结果是4*1000=4000
std::cout << "最终共享变量值: " << shared_counter << std::endl;
return 0;
}
编译运行后,输出的结果会是4000,说明自旋锁正确保护了共享变量的修改操作,没有出现数据竞争。
自旋锁的适用场景和注意事项
适用场景
- 临界区的执行时间非常短,通常在微秒级别,忙等的开销小于线程休眠唤醒的开销。
- 线程竞争不激烈,不会长时间占用CPU循环等待。
- 不能休眠的场景,比如中断处理、内核态代码等。
注意事项
- 不要在单核CPU上使用自旋锁,因为单核下持有锁的线程无法同时运行,等待线程会一直占用CPU空转,浪费资源。
- 临界区不要包含可能阻塞的操作,比如IO、内存分配等,否则会导致等待线程长时间忙等,占用大量CPU。
- 自旋锁不支持递归获取,同一个线程连续调用两次
lock会导致死锁。 - 如果竞争比较激烈,建议使用互斥锁
std::mutex,避免大量CPU资源浪费在忙等上。
带退避策略的自旋锁优化
基础的自旋锁在竞争激烈时会大量占用CPU,我们可以加入退避策略,当多次获取锁失败后,让线程短暂让出CPU,减少资源浪费:
#include <atomic>
#include <thread>
#include <chrono>
class BackoffSpinLock {
private:
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
BackoffSpinLock(const BackoffSpinLock&) = delete;
BackoffSpinLock& operator=(const BackoffSpinLock&) = delete;
BackoffSpinLock() = default;
void lock() {
int retry_count = 0;
while (flag.test_and_set(std::memory_order_acquire)) {
++retry_count;
// 重试次数较少时,短暂让出CPU
if (retry_count < 10) {
std::this_thread::yield();
} else if (retry_count < 50) {
// 重试次数较多时,休眠更长时间
std::this_thread::sleep_for(std::chrono::microseconds(10));
} else {
// 重试次数非常多时,休眠更久
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
}
void unlock() {
flag.clear(std::memory_order_release);
}
bool try_lock() {
return !flag.test_and_set(std::memory_order_acquire);
}
};
这种带退避策略的自旋锁在竞争不激烈时性能和基础自旋锁接近,在竞争激烈时能减少CPU的浪费,适用性更强。
C++std::atomic_flagSpinlock忙等自旋锁修改时间:2026-06-25 20:13:02