当前位置:

首页 > 编程开发 > C++ atomic_flag实现自旋锁 _ 无锁同步机制入门【干货】

C++ atomic_flag实现自旋锁 _ 无锁同步机制入门【干货】

C++ atomic_flag实现自旋锁 | 无锁同步机制入门【干货】 atomic_flag 为什么不能直接用 operator== 判断状态 这事儿得从设计初衷说起。atomic_flag 被刻意设计成“只写不可读”的原子类型——它既不提供 load() 方法,也不允许隐式转换成 bool。为什

C++ atomic_flag实现自旋锁 | 无锁同步机制入门【干货】

C++ atomic_flag实现自旋锁 _ 无锁同步机制入门【干货】

atomic_flag 为什么不能直接用 operator== 判断状态

这事儿得从设计初衷说起。atomic_flag 被刻意设计成“只写不可读”的原子类型——它既不提供 load() 方法,也不允许隐式转换成 bool。为什么这么“不近人情”?目的很明确:就是为了强制开发者必须通过“测试并置位”(test_and_set())这个原子操作来建模自旋逻辑。很多新手会下意识地写 if (flag == false),结果编译直接报错。这可不是语言在刁难你,而是在善意提醒:别试图绕过原子语义,那会引入数据竞争。

正确的打开方式只有一种:依赖 test_and_set() 的返回值,它返回的是操作前的旧值,并且默认使用最强的顺序一致性内存序(memory_order_seq_cst):

std::atomic_flag flag = ATOMIC_FLAG_INIT;
// 想知道锁是否空闲?只能靠“试”:
while (flag.test_and_set(std::memory_order_acquire)) {
    // 在这里自旋等待,或者考虑加入适度的退让策略
}
  • 初始化必须使用宏 ATOMIC_FLAG_INIT。如果尝试用空的花括号 {} 或 = {},尤其在静态存储期对象上,可能导致未定义行为。
  • test_and_set() 这个操作有个固定动作:总是把标志设为 true。它返回的是“设置之前”的值。所以,如果第一次调用返回 false,恭喜你,抢锁成功。
  • 自旋循环里要慎用 std::this_thread::yield()。它并不保证会让出CPU,在某些平台上可能等同于空转。真想降低CPU占用,可以考虑短暂的休眠或指数退避策略。

自旋锁构造函数里忘记 clear() 会导致首次 lock() 永远阻塞

这是一个经典的坑。新创建的 atomic_flag 对象,其初始状态是“未指定的”(unspecified),而不是自动为 false。如果你跳过了初始化步骤,那么第一次调用 test_and_set() 就有可能返回 true,导致锁永远无法获取。

安全的构造函数写法主要有两种:

立即学习“C++免费学习笔记(深入)”;

struct spinlock {
    std::atomic_flag flag;
    spinlock() : flag(ATOMIC_FLAG_INIT) {} // ✅ 推荐:在成员初始化列表里完成
    // 或者另一种写法:
    // spinlock() { flag.clear(std::memory_order_relaxed); }
};
  • clear() 是唯一能将 atomic_flag 设为 false 的方法,必须显式调用。ATOMIC_FLAG_INIT 这个宏,展开后本质上就是提供了 ATOMIC_VAR_INIT(false) 级别的初始化保障。
  • 注意,不要在类内直接写 std::atomic_flag flag{ATOMIC_FLAG_INIT}。C++11 不支持非静态数据成员的花括号初始化(C++14 起允许,但仍需注意ABI兼容性风险)。
  • 如果锁需要重复使用(即 unlock 之后再次 lock),那么每次 unlock 时都必须调用 flag.clear(std::memory_order_release),否则下一次 lock 必然失败。

memory_order 选错会让自旋锁在多核上失效

自旋锁的目标远不止“避免线程阻塞”那么简单,它的核心使命是保证临界区内的内存访问顺序不被编译器和处理器重排,同时确保缓存一致性。一个最常见的错误就是全部使用最宽松的 memory_order_relaxed:

// ❌ 危险操作:临界区内的读写可能被重排到 lock() 之前,或拖到 unlock() 之后
while (flag.test_and_set(std::memory_order_relaxed)) {}
// ... 临界区代码 ...
flag.clear(std::memory_order_relaxed);

正确的内存序组合应该是:

  • test_and_set(std::memory_order_acquire):这是一个“获取”操作,确保该操作之后的所有读写都不会被重排到它前面去。
  • clear(std::memory_order_release):这是一个“释放”操作,确保该操作之前的所有读写都不会被重排到它后面去。
  • 这一对“获取-释放”操作共同构成了一个同步点,使得不同线程能观察到一致的修改顺序。

从性能角度看,acquire/release 在 x86/x64 架构上几乎没有额外开销(依赖硬件内存屏障)。但在 ARM/AArch64 架构上,它们会生成类似 dmb ish 的指令——这笔开销绝对不能省,这是正确性的代价。

为什么不用 atomic_bool 替代 atomic_flag 实现自旋锁

当然可以这么做,但通常不建议,因为它容易引入一些隐蔽的bug。有人为了图方便会写成这样:

std::atomic flag{false};
while (flag.exchange(true, std::memory_order_acquire)) {} // ❌ 潜在问题!

这里存在几个关键差异:

  • exchange() 是一个“读-改-写”操作,而 atomic_flag::test_and_set() 通常对应更底层的原子指令(在 x86 上可能是 XCHG 或 LOCK BTS)。
  • 更重要的是语义保证:atomic_flag 标准要求在所有平台上都必须是“无锁”(lock-free)实现的,绝不会在底层偷偷使用互斥量。而 atomic 在某些特定平台(例如一些旧的 ARMv7 实现)上,有可能退回到基于互斥锁的实现,那这就不是真正的“自旋”锁了。
  • atomic_flag 通常也更轻量,没有额外的填充字节和对齐冗余,sizeof(std::atomic_flag) 往往是 1 个字节。

如果确实想用 atomic,务必先调用 is_lock_free() 确认其底层实现。并且,exchange 操作的内存序参数需要仔细配对(例如 acquire 配对 release),不能只用一个内存序了事。

说到底,实现一个自旋锁的代码不过四五行,真正的难点在于理解 test_and_set() 返回值与内存序之间那份脆弱的契约。漏掉其中任何一环,程序可能在99%的机器上运行无误,却在剩下的1%场景下出现死锁或静默的数据错误。这才是最需要警惕的地方。

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发 C++
相关文章 更多
C++动态数组初始化怎么写?常用语句与代码示例
C++动态数组初始化怎么写?常用语句与代码示例

深入解析C++中动态数组的初始化机制,涵盖new操作符的不同用法、基本类型与类对象的初始化差异,以及为何在现代C++开发中应优先使用std::vector。

C++类构造与析构函数详解
C++类构造与析构函数详解

C++类构造与析构函数详解 C++这门语言,可以说是从C语言这棵大树上衍生出的高级果实,如今的应用普及度有目共睹。作为一种静态类型的通用编程语言,它厉害的地方在于融合了多种编程哲学——无论是传统的面向过程,还是主流的面向对象,乃至数据抽象、泛型编程这些高级概念,它都能很好地支持。正因为这份卓越的扩展

C++中std::upper
C++中std::upper

C++中std::upper_bound用法解析 在C++标准模板库(STL)的算法工具箱里,upper_bound() 绝对算得上是一把精准的“探针”。它的核心任务很明确:在一个已经排好序的区间 [first, last) 内,帮你快速定位到第一个**严格大于**指定值 value 的那个元素。这

C++常对象与成员解析
C++常对象与成员解析

C++中“常”概念全景解析:从对象、成员到指针与引用 在C++的世界里,“常量性”是一个强大的保障机制。它不仅仅是一个const关键字那么简单,而是构建健壮、安全程序的重要基石。今天,我们就来系统梳理一下围绕“常”的一系列概念:常成员、常对象、常指针与常引用。理解它们,是写出高质量C++代码的关键一

c++如何将结构体写入SQLite数据库_对象关系映射方案【进阶】
c++如何将结构体写入SQLite数据库_对象关系映射方案【进阶】

直接用SQLiteCAPI绑定结构体需要手动处理字段展平与顺序对齐,ORM库虽省代码但存在隐式行为风险。小项目建议原生绑定,中大型项目可搭配编译期校验使用轻量ORM。绑定std::string需传指针与长度,注意生命周期;浮点字段比较需设误差范围。

什么是编程,学习编程的意义是?
什么是编程,学习编程的意义是?

编程是编写程序指令,让设备按意图运行,常用Python、C等语言或Scratch图形化编程。学习编程能跟上科技时代,锻炼自学与解决问题能力,提升逻辑思维,代表行动力,推动事物前进。各司其职,编程者尽本分。

编程是门艺术_是编程艺术吗?
编程是门艺术_是编程艺术吗?

编程被争论是否为艺术。从定义看,编程是技能表达,但并非所有编程都是应用开发。写代码如音乐、文章,有独特风格。好代码需耐心与逻辑能力。编程是艺术,但应用开发不等同于编程艺术。

【编程的感受】
【编程的感受】

很多人提起编程,总觉得这是一门“不起眼”的学问——不就是写写代码嘛,能有多深?但真走进去了才会发现,编程里头的门道,远比表面看上去要丰富得多。 学习编程的过程中,最容易卡住的往往是那些基础语法。就拿那个常见的 for 循环来说吧,刚开始时怎么都绕不过弯来:为什么这么写能循环?循环条件到底怎么控制?一

对于懂得编程的人来说,编程对你来说有什么乐趣?编程大概是什么感觉?
对于懂得编程的人来说,编程对你来说有什么乐趣?编程大概是什么感觉?

解决编程问题后的痛快、完成作品的成就感、获取知识的满足感,以及用代码实现简化生活的惊喜与踏实,这些是编程独有的乐趣,只有真正写代码的人才能体会其中的酸甜苦辣、惊喜与满足,令人回味无穷。

趣味编程:少儿编程思维的启蒙之旅
趣味编程:少儿编程思维的启蒙之旅

Scratch图形化编程以拖拽积木块方式启蒙少儿编程思维,无需记忆代码语法。通过素材库、即时反馈和社区协作,帮助孩子在创作中理解顺序执行、条件判断等核心概念,锻炼逻辑、创造力和问题解决能力。

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程
PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
Postman macOS版
Postman macOS版
Mac

Postman是一套面向API全生命周期的开发与测试平台,可在Mac上发送和调试多种协议请求,管理集合、环境及认证信息,并完成脚本测试、模拟服务、监控、文档发布和团队协作。它适合开发、测试及运维人员使用,提供个人免费方案。

Docker Desktop macOS版
Docker Desktop macOS版
Mac

Docker Desktop 是 Docker 官方为 Mac 提供的容器开发环境,集成 Docker Engine、命令行工具、Compose、Build、Kubernetes 与图形化管理界面。

Android Studio macOS版
Android Studio macOS版
Mac

Android Studio 是 Google 面向安卓应用开发提供的官方集成开发环境,基于 IntelliJ IDEA 构建。它整合智能代码编辑、Gradle 构建、界面预览、设备模拟、性能分析、调试测试及 APK 分析等工具。

WINDOWS 更多
PixPix 网页版
PixPix 网页版
Windows

PixPix 是面向品牌、卖家和内容团队的在线 AI 图片视频创作平台,提供图视频 Agent、无限画布、图片生成、视频生成、自然语言编辑及商品套图、产品精修和带货视频等电商工具。

Postman Windows版
Postman Windows版
Windows

Postman 是面向 API 全生命周期的开发与协作平台。Windows 桌面版可用于构建和发送 HTTP、GraphQL、gRPC、WebSocket、MQTT 等请求,并提供环境变量、集合、脚本测试、模拟服务、监控、文档及团队工作区等

Docker Desktop Windows版
Docker Desktop Windows版
Windows

Docker Desktop 为 Windows 开发者提供集成式容器环境,内置 Docker Engine、命令行工具、Compose、Build 与 Kubernetes 等组件,可在本机完成镜像构建、容器运行、日志查看和资源管理。