【C/C++】自旋锁 Spin Lock
2026/9/16 1:13:49 网站建设 项目流程

自旋锁(Spinlock)详解

什么是自旋锁?

自旋锁是一种轻量级的同步机制。当线程尝试获取锁但锁已被占用时,线程不会进入睡眠状态,而是在原地"自旋"(忙等待),不断检查锁是否可用。

与互斥锁(mutex)的区别在于:互斥锁在获取失败时会让线程睡眠,涉及操作系统调度和上下文切换;自旋锁则保持线程运行,持续尝试获取锁。

适用场景

自旋锁适合以下情况:

临界区非常短,预期锁很快就会被释放。上下文切换的开销比自旋等待更大。在高频交易(HFT)等对延迟极度敏感的系统中尤其常见。

不适合的情况包括:临界区较长、锁竞争激烈、或者线程数远超 CPU 核心数。

基础实现

最简单的自旋锁可以用std::atomic_flag实现:

#include<atomic>classSpinlock{std::atomic_flag flag=ATOMIC_FLAG_INIT;public:voidlock(){while(flag.test_and_set(std::memory_order_acquire));}voidunlock(){flag.clear(std::memory_order_release);}};

test_and_set是一个原子操作,它返回 flag 当前的值,并将 flag 设置为 true。如果返回 false,说明之前没有人持有锁,我们成功获取;如果返回 true,说明锁已被占用,继续自旋。

内存序的选择

为什么用memory_order_acquirememory_order_release

acquire语义确保:在获取锁之后的所有读写操作,不会被重排到获取锁之前。这样我们能看到前一个持有者在临界区内的所有写入。

release语义确保:在释放锁之前的所有读写操作,不会被重排到释放锁之后。这样下一个获取锁的线程能看到我们在临界区内的所有写入。

这两者配合,形成了一个同步点,保证临界区的可见性。

性能问题:缓存行颠簸

上面的基础实现有一个问题。在高竞争情况下:

while(flag.test_and_set(std::memory_order_acquire));

test_and_set每次都会写入 flag,即使锁已经被占用。多个核心同时写同一个缓存行会导致缓存行在核心之间不断传递,这叫做"缓存行颠簸"或"乒乓效应",严重影响性能。

优化:Test-and-Test-and-Set (TTAS)

改进方案是先只读检查,只有当锁看起来可用时才尝试获取:

voidlock(){while(true){// 第一步:只读等待while(flag.test(std::memory_order_relaxed));// 第二步:尝试获取if(!flag.test_and_set(std::memory_order_acquire))return;}}

test是只读操作,不会使其他核心的缓存行失效。多个线程可以同时在本地缓存上自旋,不产生总线流量。只有当锁释放时,缓存行才会更新,线程才会尝试test_and_set

进一步优化:PAUSE 指令

在自旋循环中加入pause指令可以进一步优化:

#include<immintrin.h>voidlock(){while(true){while(flag.test(std::memory_order_relaxed)){_mm_pause();}if(!flag.test_and_set(std::memory_order_acquire))return;}}

_mm_pause是 x86 的 PAUSE 指令,它告诉 CPU “我在自旋等待”。好处包括:降低功耗、减少流水线刷新、在超线程环境下让出资源给兄弟线程。延迟大约 10-40 个时钟周期,具体取决于 CPU 型号。

如果用 GCC 或 Clang,也可以用__builtin_ia32_pause()代替,不需要额外头文件。

跨平台考虑

PAUSE 指令是 x86 特有的。ARM 架构有类似的 YIELD 指令:

#ifdefined(__x86_64__)||defined(_M_X64)_mm_pause();#elifdefined(__aarch64__)asmvolatile("yield");#endif

混合策略

如果锁可能被持有较长时间,可以采用混合策略。先自旋一段时间,如果还没获取到就让出 CPU:

voidlock(){// 先自旋for(inti=0;i<1000;i++){if(!flag.test(std::memory_order_relaxed)){if(!flag.test_and_set(std::memory_order_acquire))return;}_mm_pause();}// 自旋太久,退让while(flag.test_and_set(std::memory_order_acquire)){std::this_thread::yield();}}

yield会让出当前时间片,让操作系统调度其他线程。开销比 pause 大得多(微秒级 vs 纳秒级),但避免了长时间空转。

公平性问题

上面的实现都是不公平的。多个线程竞争时,不保证先来的先获取。在极端情况下可能导致某些线程饥饿。

如果需要公平性,可以实现票据锁(Ticket Lock):

#include<atomic>#include<immintrin.h>classTicketLock{std::atomic<size_t>next_ticket{0};std::atomic<size_t>now_serving{0};public:voidlock(){size_t my_ticket=next_ticket.fetch_add(1,std::memory_order_relaxed);while(now_serving.load(std::memory_order_acquire)!=my_ticket){_mm_pause();}}voidunlock(){now_serving.fetch_add(1,std::memory_order_release);}};

每个线程取一个号码,按号码顺序获取锁。严格先来先服务。

完整代码

最终优化版本:

#include<atomic>#include<immintrin.h>classSpinlock{std::atomic_flag flag=ATOMIC_FLAG_INIT;public:voidlock(){while(true){// 只读自旋,避免缓存行颠簸while(flag.test(std::memory_order_relaxed)){_mm_pause();}// 尝试获取if(!flag.test_and_set(std::memory_order_acquire))return;}}voidunlock(){flag.clear(std::memory_order_release);}};

总结

实现自旋锁需要注意以下几点。使用正确的内存序保证可见性。用 TTAS 模式避免缓存行颠簸。在自旋循环中加入 PAUSE 指令。根据场景选择是否需要公平性。明确自旋锁的适用场景,不要滥用。

在高频交易等延迟敏感场景,自旋锁比互斥锁更合适,因为它避免了上下文切换的开销。但在锁竞争激烈或临界区较长的情况下,互斥锁可能是更好的选择。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询