【C++】锁与原子操作(四):自旋锁的完整实现与性能优化

发布时间:2026/11/12 16:41:16

【C++】锁与原子操作(四):自旋锁的完整实现与性能优化 前面已经介绍了原子操作、CAS 和 C 内存序。本文继续分析一个完整的自旋锁实现。自旋锁的核心思想是当锁已经被其他线程占用时当前线程不进入休眠而是在原地循环检查锁的状态直到锁被释放。一个基础自旋锁主要需要提供以下接口spinlock_init(); // 初始化自旋锁 spinlock_lock(); // 阻塞式获取锁 spinlock_trylock(); // 尝试获取锁 spinlock_unlock(); // 释放锁 spinlock_destroy(); // 销毁锁本文将结合这些接口完整分析自旋锁从创建到使用的过程。一、自旋锁是如何表示锁状态的自旋锁只需要保存一个简单的状态0锁当前空闲 1锁已经被某个线程占用因此可以使用一个原子整数表示锁#include atomic struct spinlock { // 0表示锁空闲1表示锁已经被占用 std::atomic_int lock; };初始化时将锁设置为0static inline void spinlock_init( struct spinlock* lock) { // 初始化为0表示当前没有线程持有锁 std::atomic_init(lock-lock, 0); }多个线程争抢锁时目标都是尝试完成下面的修改lock0 → 1由于这个修改必须具有原子性因此不能使用普通赋值// 错误检查和修改不是一个原子操作 if (lock-lock 0) { lock-lock 1; }假设两个线程同时执行上面的代码线程A读取lock0 线程B读取lock0 线程A将lock改成1 线程B也将lock改成1两个线程都会认为自己获得了锁最终同时进入临界区自旋锁便失去了作用。正确做法是使用原子交换lock-lock.exchange( 1, std::memory_order_acquire );exchange()会完成两件事将原子变量设置成新值1 返回修改前的旧值。例如std::atomicint value{0}; int old_value value.exchange(1);执行以后old_value 0 value 1因此可以根据旧值判断是否成功获得锁int old_value lock-lock.exchange(1); if (old_value 0) { // 原来是空闲状态本线程成功获得锁 } else { // 原来已经是1说明锁被其他线程占用 }二、自旋锁为什么使用双层循环最简单的自旋锁可以写成static inline void spinlock_lock( struct spinlock* lock) { while (lock-lock.exchange( 1, std::memory_order_acquire)) { // 获取失败后不断重新尝试 } }这段代码能够保证正确性但存在一个性能问题。只要锁没有被释放所有等待线程都会不断执行lock-lock.exchange(1);exchange()不只是读取锁状态还会尝试执行写操作。多个处理器核心不断写同一个原子变量会频繁争抢该变量所在的缓存行增加缓存一致性开销。因此可以将加锁过程分成两步第一步使用exchange真正尝试获取锁 第二步失败后先使用普通原子读取等待 直到观察到锁变为0再重新使用exchange。完整实现如下static inline void spinlock_lock( struct spinlock* lock) { for (;;) { /* * exchange将lock设置为1 * 同时返回修改前的值。 * * 返回0 * 原来的锁是空闲状态加锁成功。 * * 返回1 * 锁已经被其他线程占用加锁失败。 */ if (!lock-lock.exchange( 1, std::memory_order_acquire)) { return; } /* * 获取锁失败后不要立即重复执行exchange。 * * 这里只使用load读取锁状态 * 等待锁从1恢复成0。 */ while (lock-lock.load( std::memory_order_relaxed)) { // 当前线程继续自旋 } } }这种方式通常称为Test-and-Test-and-Set 先读状态再尝试修改整个执行过程为执行exchange尝试加锁 ↓ 返回0 ↓ 成功获得锁 执行exchange尝试加锁 ↓ 返回1 ↓ 使用load循环读取锁状态 ↓ 发现锁变成0 ↓ 重新执行exchange争抢锁为什么不能只通过load()判断锁为空然后直接进入临界区// 错误示例 while (lock-lock.load() ! 0) { } lock-lock.store(1); // 进入临界区因为从load()看到锁为空到执行store(1)之间其他线程也可能同时观察到锁为空。因此load只能用于等待 真正获取锁仍然必须使用exchange或CAS。三、_mm_pause()为什么能优化自旋等待在 x86-64 平台上自旋循环中通常会加入_mm_pause();需要包含头文件#include immintrin.h优化后的自旋锁如下static inline void spinlock_lock( struct spinlock* lock) { for (;;) { if (!lock-lock.exchange( 1, std::memory_order_acquire)) { return; } while (lock-lock.load( std::memory_order_relaxed)) { /* * 告诉处理器当前处于自旋等待状态 * 减少空循环带来的处理器资源消耗。 */ _mm_pause(); } } }_mm_pause()并不会让线程真正休眠也不会主动把 CPU 交给其他线程。它更像是向处理器发出提示当前代码正在等待一个共享状态发生变化不需要按照普通计算循环积极执行。在支持超线程的处理器上同一个物理核心可能同时运行两个逻辑线程。如果一个逻辑线程执行紧密的空循环while (lock-lock.load()) { }它可能占用较多执行资源影响同一物理核心上的另一个逻辑线程。加入_mm_pause()后可以适当降低这种影响。为了兼容不同平台可以使用宏#if defined(__x86_64__) #include immintrin.h // x86-64平台使用pause指令 #define atomic_pause_() _mm_pause() #else // 其他平台暂时不执行额外操作 #define atomic_pause_() ((void)0) #endif加锁函数可以统一写成static inline void spinlock_lock( struct spinlock* lock) { for (;;) { if (!lock-lock.exchange( 1, std::memory_order_acquire)) { return; } while (lock-lock.load( std::memory_order_relaxed)) { atomic_pause_(); } } }需要注意_mm_pause()只能降低自旋等待的部分成本不能改变自旋锁本身的特点。如果锁被持有很长时间等待线程仍然会不断占用 CPU。四、尝试加锁、解锁与兼容封装1.spinlock_trylock()普通的spinlock_lock()会一直等待直到成功获得锁。有些场景并不希望线程阻塞式自旋而是只尝试一次if (spinlock_trylock(lock)) { // 成功获得锁 } else { // 锁正被占用执行其他操作 }实现如下static inline int spinlock_trylock( struct spinlock* lock) { /* * 第一步先读取锁状态。 * * 如果当前已经是1就没有必要立即执行exchange。 */ if (lock-lock.load( std::memory_order_relaxed)) { return 0; } /* * 即使刚才读取到0也不能直接认为加锁成功。 * * 因为其他线程可能在load之后抢先获得锁 * 所以仍然需要通过exchange完成最终竞争。 */ return !lock-lock.exchange( 1, std::memory_order_acquire ); }也可以写成static inline int spinlock_trylock( struct spinlock* lock) { return !lock-lock.load( std::memory_order_relaxed) !lock-lock.exchange( 1, std::memory_order_acquire); }这里利用了逻辑与的短路特性。如果第一次读取发现锁已经是1后面的exchange()不会执行。假设两个线程同时调用trylock()并且都先读取到0线程Aload得到0 线程Bload得到0它们随后都会执行exchange(1)。但原子交换保证只有一个线程能够看到旧值为0线程Aexchange返回0加锁成功 线程Bexchange返回1加锁失败因此前面的load()只是性能优化真正保证正确性的仍然是后面的原子交换。2.spinlock_unlock()解锁时将锁从1设置回0static inline void spinlock_unlock( struct spinlock* lock) { lock-lock.store( 0, std::memory_order_release ); }为什么加锁使用acquire解锁使用release加锁 acquire 保证获得锁以后能够看到上一个持锁线程 在解锁前对共享数据完成的修改。 解锁 release 保证当前线程临界区中的修改 在锁变为0之前完成并发布。例如int shared_data 0; spinlock lock; void writer() { spinlock_lock(lock); // 临界区内修改共享数据 shared_data 100; spinlock_unlock(lock); } void reader() { spinlock_lock(lock); /* * 成功获得锁以后可以看到 * 上一个线程释放锁前写入的shared_data。 */ int value shared_data; spinlock_unlock(lock); }锁上的release和后续成功加锁的acquire建立同步关系从而保护临界区中的普通共享数据。3.spinlock_destroy()原子变量本身不需要释放操作系统资源因此销毁函数可以为空static inline void spinlock_destroy( struct spinlock* lock) { /* * 当前原子自旋锁没有动态内存 * 也没有需要销毁的系统锁对象。 */ (void)lock; }虽然函数没有真正执行销毁操作但保留统一接口有两个好处调用代码结构保持一致 以后切换成pthread互斥锁时不需要修改使用方式。4. 使用宏统一调用方式可以定义几个辅助宏#define SPIN_INIT(object) \ spinlock_init((object)-lock) #define SPIN_LOCK(object) \ spinlock_lock((object)-lock) #define SPIN_UNLOCK(object) \ spinlock_unlock((object)-lock) #define SPIN_DESTROY(object) \ spinlock_destroy((object)-lock)例如有一个任务队列struct task_queue { spinlock lock; int task_count; };可以这样使用task_queue queue; // 初始化queue中的自旋锁 SPIN_INIT(queue); // 加锁 SPIN_LOCK(queue); queue.task_count; // 解锁 SPIN_UNLOCK(queue); // 销毁 SPIN_DESTROY(queue);这样上层代码不需要反复编写spinlock_lock(queue.lock);五、完整代码、兼容方案与使用场景下面给出一个可以直接使用的自旋锁头文件#ifndef SIMPLE_SPINLOCK_H #define SIMPLE_SPINLOCK_H #include atomic #if defined(__x86_64__) || defined(_M_X64) #include immintrin.h /* * x86-64平台使用pause指令 * 优化紧密的自旋等待循环。 */ #define SPIN_PAUSE() _mm_pause() #else #define SPIN_PAUSE() ((void)0) #endif struct spinlock { // 0表示空闲1表示已被占用 std::atomic_int lock; }; /** * 初始化自旋锁 */ static inline void spinlock_init( struct spinlock* lock) { std::atomic_init(lock-lock, 0); } /** * 阻塞式获取自旋锁 */ static inline void spinlock_lock( struct spinlock* lock) { for (;;) { /* * 真正尝试将锁从0修改为1。 */ if (!lock-lock.exchange( 1, std::memory_order_acquire)) { return; } /* * 获取失败后先使用只读方式等待。 */ while (lock-lock.load( std::memory_order_relaxed)) { SPIN_PAUSE(); } } } /** * 尝试获取自旋锁 * * 成功返回1 * 失败立即返回0。 */ static inline int spinlock_trylock( struct spinlock* lock) { return !lock-lock.load( std::memory_order_relaxed) !lock-lock.exchange( 1, std::memory_order_acquire); } /** * 释放自旋锁 */ static inline void spinlock_unlock( struct spinlock* lock) { lock-lock.store( 0, std::memory_order_release ); } /** * 原子自旋锁不持有额外系统资源 */ static inline void spinlock_destroy( struct spinlock* lock) { (void)lock; } #endif下面使用自旋锁保护 4 个线程共同累加一个普通整数#include iostream #include thread #include spinlock.h // 全局自旋锁 spinlock count_lock; // 被多个线程共同修改的普通变量 int count 0; /** * 将count增加指定次数 */ void increase(int number) { for (int i 0; i number; i) { // 进入临界区前加锁 spinlock_lock(count_lock); count; // 离开临界区后解锁 spinlock_unlock(count_lock); } } int main() { // 使用前初始化自旋锁 spinlock_init(count_lock); std::thread t1(increase, 500); std::thread t2(increase, 500); std::thread t3(increase, 500); std::thread t4(increase, 500); t1.join(); t2.join(); t3.join(); t4.join(); std::cout count count \n; // 使用结束后调用统一销毁接口 spinlock_destroy(count_lock); return 0; }最终结果为count 2000除了标准原子操作还可以为不支持标准原子库的编译环境提供编译器内置函数版本typedef int atomic_flag_; #define ATOMIC_FLAG_INIT_ 0 #define atomic_flag_test_and_set_(ptr) \ __sync_lock_test_and_set(ptr, 1) #define atomic_flag_clear_(ptr) \ __sync_lock_release(ptr) struct spinlock { atomic_flag_ lock; }; static inline void spinlock_init( struct spinlock* lock) { lock-lock ATOMIC_FLAG_INIT_; } static inline void spinlock_lock( struct spinlock* lock) { while (atomic_flag_test_and_set_( lock-lock)) { } } static inline int spinlock_trylock( struct spinlock* lock) { return atomic_flag_test_and_set_( lock-lock) 0; } static inline void spinlock_unlock( struct spinlock* lock) { atomic_flag_clear_(lock-lock); }还可以通过编译宏将同一套接口切换为互斥锁#include pthread.h struct spinlock { pthread_mutex_t lock; }; static inline void spinlock_init( struct spinlock* lock) { pthread_mutex_init( lock-lock, nullptr ); } static inline void spinlock_lock( struct spinlock* lock) { pthread_mutex_lock(lock-lock); } static inline int spinlock_trylock( struct spinlock* lock) { return pthread_mutex_trylock( lock-lock) 0; } static inline void spinlock_unlock( struct spinlock* lock) { pthread_mutex_unlock(lock-lock); } static inline void spinlock_destroy( struct spinlock* lock) { pthread_mutex_destroy(lock-lock); }这样上层代码始终使用spinlock_lock(lock); spinlock_unlock(lock);底层既可以选择原子自旋锁也可以选择pthread_mutex。使用自旋锁时需要注意以下问题1. 自旋锁适合非常短的临界区 2. 临界区中不要进行网络请求、文件读写和休眠 3. 等待期间线程会继续占用CPU 4. 高竞争情况下互斥锁可能比自旋锁更合适 5. 自旋锁通常不保证线程获取锁的公平性 6. 同一个线程重复获取普通自旋锁可能造成死锁 7. 成功加锁后必须保证最终执行解锁 8. 对简单计数器直接使用atomic通常更加合适。这一篇的核心可以总结为自旋锁使用原子变量表示锁状态 exchange负责真正争抢锁 load负责在锁被占用时低成本等待 双层自旋可以减少频繁原子写造成的缓存竞争 _mm_pause可以优化处理器中的紧密等待循环 加锁使用acquire解锁使用release trylock只尝试一次失败后立即返回 统一接口可以在自旋锁和互斥锁之间切换。0voice · GitHub
延伸阅读

更多相关文章

2026/11/11 17:50:18

Windows图片密码:用个性化手势解锁,提升安全与体验

你有没有过这样的体验:每天打开电脑,输入那串早已烂熟于心、却又毫无生气的数字或图案密码,感觉就像在完成一项枯燥的例行公事?或者,在公共场合输入密码时,总担心身后有双眼睛在窥探?我们习惯了…

2026/11/11 19:31:45

华为OD机考整数编码题解析:从变长编码原理到Python实现

1. 项目概述:从一道机试题看华为OD的编码思维最近在帮几个准备华为OD机考的朋友做模拟练习,发现“整数编码”这道题出现的频率相当高,无论是C卷还是Python方向的机试,都绕不开它。这道题本身并不复杂,但非常典型&#…

2026/11/11 22:17:47

Flink部署模式全解析:从本地单机到YARN集群实战指南

1. 项目概述:从单机到集群,Flink部署的必经之路搞流计算的朋友,Flink是绕不开的一个选择。无论是想快速验证一个数据处理逻辑,还是在生产环境构建一个高可用的实时计算集群,第一步都是把Flink环境给搭起来。很多新手卡…

2026/11/12 16:36:55

定襄县电工证在哪报名保姆级教程:学历不高也能轻松拿证

定襄县电工证在哪报名保姆级教程:学历不高也能轻松拿证 你是不是也担心自己学历不高,报不了电工证?别急,今天我就给你讲清楚, 定襄县电工证在哪报名 这事儿,其实一点也不复杂, 保姆级教程 给你安排得明明白白,学历不是问题,关键是方法对不对。…

2026/11/12 16:36:55

山东特种工证件查询别再被坑了!内部消息教你搞定复审延期

山东特种工证件查询别再被坑了!内部消息教你搞定复审延期 证书过期了不知道怎么复审延期?别急,我来帮你解决。今天我以山东特种工证件查询为核心,结合我多年在应急管理一线的经验,分享几个真实案例,带你了解内部消息,避免走弯路。…

2026/11/12 16:36:55

绍兴高压电工证保姆级教程:学历不高怕报不上名?看完这篇就懂了

绍兴高压电工证保姆级教程:学历不高怕报不上名?看完这篇就懂了 你是不是学历不高,担心自己考不了绍兴高压电工证?别急,今天这整篇保姆级教程,就是为像你一样的人量身打造的。不管你是刚毕业、想转行,还是想提升技能,这篇内容都能帮你理清思路,少走弯…

2026/11/12 16:36:55

梓潼考电工证在哪里值不值得考?电工证过期了怎么处理

梓潼考电工证在哪里值不值得考?电工证过期了怎么处理 证书过期了不知道怎么复审延期,这事儿可真让人头疼。尤其对于那些在工地、工厂一线干活的电工来说,证书就是“饭碗”的保障,一旦过期没及时处理,轻则影响工作,重则面临罚款、停工甚至法律责任。今天…

2026/11/12 16:31:53

有限空间作业证规定最新解读,电子证书怎么查才靠谱

有限空间作业证规定最新解读,电子证书怎么查才靠谱 怕考不过白交培训费?现在办理有限空间作业证,很多人第一反应是怕钱花了证没拿到,更怕拿了证却发现不符合规定、不能用。别急,今天咱们就从【有限空间作业证规定】入手,讲清楚电子证书怎么查、怎么用,…

2026/11/11 12:42:59

电工证丢失还能补吗?千万别踩坑,丢了证等于失业

电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。…

2026/11/12 0:04:01

工地太忙根本没时间复习考试?学习电工证到哪里学通过率揭秘

工地太忙根本没时间复习考试?学习电工证到哪里学通过率揭秘 工地太忙根本没时间复习考试?你不是一个人。作为一名在南阳卧龙区干了七八年市政公用工程的电工,我深知这个问题的现实性。电工证不仅是工作的“入场券”,更是职业发展的“通行证”,但备考过程…

2026/11/12 0:04:01

双流电工证考别怕挂科!郑州报考避坑指南全攻略

双流电工证考别怕挂科!郑州报考避坑指南全攻略 实操考试心里没底怕挂科?双流电工证考在郑州报名,很多人因为不了解流程、准备不充分,白白浪费时间和金钱。作为三门峡应届工程类毕业生,我也曾经历过这样的焦虑。今天就从零开始,手把手教你如何避开郑州报…

2026/11/12 0:04:01

之前考的证在外省能不能转过来 怎么报名考电工证培训价格多少

之前考的证在外省能不能转过来 怎么报名考电工证培训价格多少 之前考的证在外省能不能转过来?这个问题确实让不少准备换城市工作的朋友犯了难,尤其像电工证这种实操性很强的证书,很多人担心跨省后证书能不能继续用。其实,只要符合相关流程,是可以顺利转…

2026/11/11 20:27:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/12 9:51:42

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑