当前位置: 首页 > news >正文

C++11并发编程:call_once一次性执行+atomic原子类型+CAS无锁编程+自旋锁

承接上一篇互斥锁与RAII锁管理,本篇聚焦于更加轻量级的同步方案

  • std::call_once:解决多线程环境下的一次性初始化问题
  • std::atomic:硬件级别的原子操作,实现无锁同步,性能远超互斥锁
  • 自旋锁:基于std::atomic_flag实现的忙等待锁,适合极短临界区场景

1:std::call_once

线程安全的一次性执行

1:为什么需要call_once

在多线程编程中,经常遇到某个函数/代码块只需要执行一次的场景:

  • 单例模式的初始化
  • 全局资源的加载
  • 配置文件的读取

如果用普通的互斥锁实现,会有两个问题

  • 每次调用都要加锁,性能开销大
  • 传统的 "双重检查锁定 (DCL)" 在 C++11 之前存在指令重排序问题,行为未定义

std::call_once是 C++11 标准库提供的线程安全的一次性执行方案,底层做了优化,性能远高于手动加锁。

2:基本用法

std::call_once需要配合std::once_flag使用:

template <class Fn, class... Args> void call_once(once_flag& flag, Fn&& fn, Args&&... args);
  • flag:一次性标志,每个需要执行一次的操作对应一个独立的once_flag
  • fn:要执行的可调用对象
  • args:传递给fn的参数

代码示例:

#include <iostream> #include <thread> #include <mutex> using namespace std; once_flag g_once_flag; void init_resource() { cout << "资源初始化完成(只执行一次)" << endl; // 模拟资源初始化 this_thread::sleep_for(chrono::milliseconds(100)); } void worker(int id) { cout << "线程" << id << "开始运行" << endl; // 只有第一个到达这里的线程会执行init_resource call_once(g_once_flag, init_resource); cout << "线程" << id << "继续执行" << endl; } int main() { thread threads[5]; for (int i = 0; i < 5; ++i) { threads[i] = thread(worker, i); } for (auto& th : threads) { th.join(); } return 0; }

3:细节

  • 异常处理:如果fn执行时抛出异常,那么call_once会认为这次执行失败,下一个到达的线程会再次尝试执行fn。只有当fn正常返回时,才会标记为 "已执行"。

  • once_flag 的生命周期once_flag的生命周期必须长于所有调用call_once的线程,否则行为未定义。通常将once_flag声明为全局变量或静态变量。

  • 不能拷贝 / 移动once_flag不支持拷贝构造、移动构造、拷贝赋值和移动赋值,每个once_flag只能对应一个一次性操作。

4:实际应用:线程安全的单例模式

这是call_once最经典的应用场景,完美解决了传统单例的线程安全问题:

class Singleton { private: Singleton() { cout << "单例对象创建" << endl; } ~Singleton() { cout << "单例对象销毁" << endl; } Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; static once_flag s_once_flag; static Singleton* s_instance; public: static Singleton* getInstance() { call_once(s_once_flag, [](){ s_instance = new Singleton(); }); return s_instance; } }; // 静态成员初始化 once_flag Singleton::s_once_flag; Singleton* Singleton::s_instance = nullptr;

2:std::atomic原子类型

1:什么是原子操作

原子操作是指不可分割的操作,要么全部执行完成,要么完全不执行,中间不会被其他线程打断。

和互斥锁的对比:

  • 互斥锁:操作系统级别的同步,会导致线程阻塞和上下文切换,开销大,但适合复杂临界区
  • 原子操作:硬件 CPU 指令级别的同步,无阻塞、无上下文切换,开销极小,但只能用于简单的操作(如计数、标志位、指针交换)

2:atomic的模版参数要求

std::atomic是一个模板类,可以实例化任何满足以下条件的类型T

  • 可平凡复制 (TriviallyCopyable):没有自定义的拷贝构造函数、析构函数等
  • 可复制构造和可复制赋值
  • 没有 cv 限定符(const/volatile)

可以用以下类型特征检查:

std::is_trivially_copyable<T>::value std::is_copy_constructible<T>::value std::is_copy_assignable<T>::value

支持的常用类型

  • 所有整数类型:intlongcharbool
  • 指针类型
  • 浮点数类型(C++20 起完全支持)

不支持的类型std::stringstd::vector等复杂类型,自定义类(除非是平凡可复制的)。

3:基本原子操作

1:load和store:原子读写
T load(memory_order order = memory_order_seq_cst) const noexcept; void store(T desired, memory_order order = memory_order_seq_cst) noexcept;
  • load():原子地读取原子变量的值
  • store():原子地将desired写入原子变量

示例:

atomic<bool> flag(false); // 线程1:设置标志 flag.store(true); // 线程2:读取标志 if (flag.load()) { // 执行操作 }
2:运算符重载

对于整数和指针类型,std::atomic重载了常用的运算符:

  • 自增自减:++--(前缀和后缀)
  • 复合赋值:+=-=&=|=^=

这些运算符都是原子操作,等价于对应的fetch_*操作。

示例:原子计数器

atomic<int> cnt(0); void add() { for (int i = 0; i < 1000000; ++i) { cnt++; // 原子自增,等价于cnt.fetch_add(1) } } int main() { thread t1(add); thread t2(add); t1.join(); t2.join(); cout << cnt << endl; // 输出2000000,正确 return 0; }

4:读-修改-写原子操作:fetch_*系列

fetch_*系列函数是原子的读 - 修改 - 写操作,执行以下步骤:

  1. 原子地读取原子变量的当前值
  2. 对当前值执行指定的修改操作
  3. 原子地将修改后的值写回原子变量
  4. 返回修改前的原始值
函数作用适用类型
fetch_add(desired)原子加整数、指针
fetch_sub(desired)原子减整数、指针
fetch_and(desired)原子按位与整数
fetch_or(desired)原子按位或整数
fetch_xor(desired)原子按位异或整数
atomic<int> x(10); int old = x.fetch_add(5); // old=10,x变为15 cout << old << " " << x << endl; // 输出10 15

5:CAS操作:无锁变成的核心

CAS(Compare And Swap,比较并交换)是无锁编程的基础,几乎所有无锁数据结构都基于 CAS 实现。

C++11 提供了两个 CAS 成员函数

bool compare_exchange_weak(T& expected, T desired, memory_order success = memory_order_seq_cst, memory_order failure = memory_order_seq_cst) noexcept; bool compare_exchange_strong(T& expected, T desired, memory_order success = memory_order_seq_cst, memory_order failure = memory_order_seq_cst) noexcept;

CAS 操作的逻辑

  1. 比较原子变量的当前值和expected的值
  2. 如果相等:将原子变量的值设置为desired,返回true
  3. 如果不相等:将expected的值更新为原子变量的当前值,返回false
1:compare_exchange_weak和compare_exchange_strong
特性compare_exchange_weakcompare_exchange_strong
虚假失败可能发生不会发生
性能更高略低
用法必须放在循环中可以不放在循环中

什么是虚假失败

在某些 CPU 架构(如 ARM)上,即使原子变量的值等于expectedcompare_exchange_weak也可能返回false。这是由于硬件实现的限制,不是逻辑错误。

因此,compare_exchange_weak必须放在循环中使用,而compare_exchange_strong可以保证在值相等时一定成功。

2:CAS的经典用法:无锁计数器
#include <atomic> #include <thread> #include <iostream> using namespace std; atomic<int> cnt(0); void add() { for (int i = 0; i < 1000000; ++i) { int old = cnt.load(); // 循环尝试CAS,直到成功 while (!cnt.compare_exchange_weak(old, old + 1)) { // 什么都不用做,old已经被更新为当前值 } } } int main() { thread t1(add); thread t2(add); t1.join(); t2.join(); cout << cnt << endl; // 输出2000000 return 0; }

注意:这个例子只是为了演示 CAS 的用法,实际中直接用cnt++更简单高效。

3:CAS的ABA问题

CAS 存在一个经典的ABA 问题

  1. 线程 1 读取原子变量的值为 A
  2. 线程 2 将原子变量的值改为 B,然后又改回 A
  3. 线程 1 执行 CAS,发现值还是 A,认为没有被修改,操作成功

但实际上,原子变量的值已经被修改过了,这在某些场景下会导致严重问题(如无锁队列)。

解决方法:使用带版本号的原子变量,每次修改时同时递增版本号,这样即使值相同,版本号不同,CAS 也会失败。

6:内存序

内存序(Memory Order)用于控制编译器和 CPU 的指令重排序以及多线程之间的内存可见性

C++11 提供了六种内存序选项,从宽松到严格依次为:

  1. memory_order_relaxed:最宽松,仅保证原子性,不提供任何同步或顺序约束
  2. memory_order_consume:保证依赖于当前加载操作的数据的可见性(实际很少使用)
  3. memory_order_acquire:保证当前操作之前的所有读写操作不会被重排序到当前操作之后(用于加载操作)
  4. memory_order_release:保证当前操作之后的所有读写操作不会被重排序到当前操作之前(用于存储操作)
  5. memory_order_acq_rel:同时具有 acquire 和 release 语义(用于读 - 修改 - 写操作)
  6. memory_order_seq_cst:最严格,全局顺序一致性,所有线程看到的操作顺序一致(默认内存序)

常用的三种内存序:

  • memory_order_relaxed

    • 仅保证操作的原子性,不保证顺序和可见性
    • 适用于不需要同步的场景,如独立的计数器
    • 示例:x.store(42, memory_order_relaxed);
  • memory_order_acquire / memory_order_release

    • 成对使用,实现线程间的同步
    • 当线程 A 用release存储一个变量,线程 B 用acquire加载同一个变量时,线程 A 中所有在release之前的写操作,在线程 B 中都是可见的
    • 典型应用:生产者 - 消费者模型
atomic<bool> ready(false); int data = 0; void producer() { data = 42; // 写数据 ready.store(true, memory_order_release); // 发布数据 } void consumer() { while (!ready.load(memory_order_acquire)) { // 等待数据准备好 } cout << data << endl; // 保证看到data=42 }

memory_order_seq_cst

  • 默认内存序,最安全,性能最差
  • 保证所有线程看到的所有原子操作的顺序是一致的
  • 适用于需要强一致性的场景

7:std::atomic_flag

std::atomic_flag是 C++11 中唯一保证无锁的原子类型,比std::atomic<bool>更轻量,性能更高。

它只有两个核心操作:

// 原子地将flag设置为true,并返回之前的值 bool test_and_set(memory_order order = memory_order_seq_cst) noexcept; // 原子地将flag设置为false void clear(memory_order order = memory_order_seq_cst) noexcept;
atomic_flag flag = ATOMIC_FLAG_INIT; // 初始化为false

3:自旋锁

1:什么是自旋锁

自旋锁是一种忙等待的锁机制:当一个线程尝试获取锁但失败时,它不会进入阻塞状态,而是会在一个循环中不断尝试获取锁,直到成功为止。

和互斥锁的对比:

特性自旋锁互斥锁
等待方式忙等待,循环尝试阻塞等待,操作系统调度
上下文切换
开销锁持有时间短:低;锁持有时间长:高锁持有时间短:高;锁持有时间长:低
适用场景极短的临界区(几十条指令以内)较长的临界区

2:自旋锁优缺点

优点

  • 没有上下文切换开销,在锁竞争不激烈且持有时间短的情况下,性能远超互斥锁
  • 实现简单,基于原子操作即可实现

缺点

  • 忙等待会占用 CPU 资源,如果锁持有时间长,会导致 CPU 利用率飙升
  • 不支持递归加锁
  • 不支持条件变量
  • 单核 CPU 上使用自旋锁可能导致死锁(因为持有锁的线程无法被抢占,等待的线程会一直自旋)

3:用std::atomic_flag实现自旋锁

基于std::atomic_flagtest_and_setclear方法,可以非常简单地实现一个自旋锁:

#include <atomic> #include <thread> #include <iostream> #include <vector> using namespace std; class SpinLock { private: // 初始化为false,表示未加锁 atomic_flag flag = ATOMIC_FLAG_INIT; public: // 加锁:循环尝试test_and_set,直到返回false(之前未加锁) void lock() { // 使用memory_order_acquire保证加锁之前的操作不会被重排序到加锁之后 while (flag.test_and_set(memory_order_acquire)) { // 空循环,自旋等待 // 可以加入CPU pause指令,减少CPU功耗 // #ifdef __x86_64__ // __asm__ __volatile__("pause"); // #endif } } // 解锁:将flag设置为false void unlock() { // 使用memory_order_release保证解锁之前的操作不会被重排序到解锁之后 flag.clear(memory_order_release); } // 禁止拷贝和移动 SpinLock(const SpinLock&) = delete; SpinLock& operator=(const SpinLock&) = delete; };

代码解释

  • lock()方法:循环调用test_and_set,如果返回false,说明之前锁是未加锁状态,当前线程成功获取锁;如果返回true,说明锁已经被其他线程持有,继续循环等待。
  • unlock()方法:调用clear将 flag 设置为false,释放锁。
  • 内存序:使用memory_order_acquirememory_order_release保证临界区的内存可见性,比默认的memory_order_seq_cst性能更好。

4:自旋锁的测试和使用

SpinLock spin_lock; int cnt = 0; void add(int n) { for (int i = 0; i < n; ++i) { spin_lock.lock(); cnt++; spin_lock.unlock(); } } int main() { const int thread_num = 4; const int per_thread = 1000000; vector<thread> threads; for (int i = 0; i < thread_num; ++i) { threads.emplace_back(add, per_thread); } for (auto& th : threads) { th.join(); } cout << "最终计数:" << cnt << endl; cout << "预期计数:" << thread_num * per_thread << endl; return 0; }

5:自旋锁的注意事项

  1. 仅适用于极短临界区:如果临界区执行时间超过 100 条指令,自旋锁的性能会急剧下降,此时应该使用互斥锁。
  2. 不要在单核 CPU 上使用:单核 CPU 上,持有锁的线程无法被抢占,等待的线程会一直自旋,浪费 CPU 时间,甚至导致死锁。
  3. 持有自旋锁时不要调用阻塞函数:如果持有自旋锁的线程调用sleep()malloc()等阻塞函数,会导致其他等待的线程长时间自旋,浪费大量 CPU 资源。
  4. 不要递归加锁:自旋锁不支持递归加锁,同一线程重复加锁会导致死锁。
  5. 加入 CPU pause 指令:在自旋循环中加入pause指令(x86 架构),可以减少 CPU 的功耗和流水线停顿,提高性能。

4:总结

  • std::call_once配合std::once_flag可以实现线程安全的一次性执行,完美解决单例模式等初始化问题,性能优于手动加锁。
  • std::atomic提供了硬件级别的原子操作,无阻塞、无上下文切换,适合简单的同步场景,是无锁编程的基础。
  • CAS(比较并交换)是无锁编程的核心,compare_exchange_weak性能更高但可能虚假失败,必须放在循环中使用;compare_exchange_strong保证成功但性能略低。
  • 内存序用于控制指令重排序和内存可见性,最常用的是memory_order_relaxedmemory_order_acquire/release,默认的memory_order_seq_cst最安全但性能最差。
  • std::atomic_flag是最轻量的原子类型,基于它可以实现简单高效的自旋锁,自旋锁适合极短临界区场景,但使用时需要注意避免 CPU 浪费。
http://www.jsqmd.com/news/931438/

相关文章:

  • Meshroom:从照片到3D模型的魔法转换,免费开源工具让创作更简单
  • 你的GPU散热真的够吗?深度学习炼丹党必看的温控监控与预警设置指南(以Ubuntu/NVIDIA为例)
  • 3D质感革命:5分钟掌握NormalMap-Online免费在线法线贴图生成器终极指南
  • 2026年只会C语言就业很差吗 C语言真的要完了吗?
  • 3种高效方法:利用OCAuxiliaryTools彻底解决黑苹果配置难题
  • B站m4s视频转换终极指南:一键将缓存视频转为MP4格式
  • 51单片机四则运算计算器完整Keil工程:矩阵键盘输入+数码管显示(含源码与HEX)
  • 越南MobiFone MFY99套餐取消全攻略:短信与App双通道详解
  • 保姆级教程:用LeRobot复现斯坦福ALOHA的ACT算法,搞定双臂分拣任务
  • STM32F103RE裸机FTP方案:88W8801 WiFi AP模式 + W25Q128文件存储
  • SourceGit:跨平台Git图形化客户端终极指南,让Git操作变得简单直观
  • AI都能一键生成网站了,还要建站系统干嘛?
  • Windows下可直接运行的SpringBoot视频剪辑工具:支持剪辑、加字幕、音画合成
  • 凯芯Cascadeteq工业级存储芯片选型国产替代psram
  • 3分钟告别百度网盘限速!免费开源下载助手让你速度飙升10倍
  • “收你们来了”!2026 6 月 - 主流 AI 编程平台全面收紧订阅
  • DriverStore Explorer:Windows驱动管理的专业清理利器
  • AI-HF_Patch:让你的AI少女游戏焕然一新的魔法工具箱
  • Anthropic 发布 Claude Code 动态工作流:季度工作几天完成,75 万行代码迁移仅需 11 天!
  • VC++6.0一键打包工具:集成InstallShield向导,自动生成Windows 9x/NT安装包
  • 基于STM32F103的T12焊台温控主板方案:含多版原理图、Arduino源码与OLED图形化菜单
  • GHelper华硕笔记本轻量控制神器:高效替代方案实战指南
  • QSPI pSRAM嵌入式存储CSS1604LS高稳定国产PSRAM工作机制与规范
  • 大学生租房系统|基于SpringBoot的大学生租房系统设计与实现(源码+数据库+文档)
  • 海口钻饰回收商户实力榜,实地测评展现各家真实水平 - 奢侈品回收测评
  • 四轮独立驱动电动汽车转弯能耗最小化转矩控制【附仿真】
  • FPGA用Modbus-RTU从机VHDL代码包,含波特率配置、CRC16校验与抗干扰UART接收模块
  • 知网维普查重 Turnitin 双适配!Okbiye 论文降重 + 降 AIGC 功能实测,解决重复率与 AI 痕迹双难题
  • 从R的clusterProfiler到Python的gseapy:手把手教你完成ORA分析并解读结果(附代码避坑)
  • 2026 厦门包包回收资源盘点,收的顶本地实体变现更高效 - 奢侈品回收测评