C++ 多线程同步之原子操作(atomic)实战

C++ 多线程同步之原子操作(atomic)实战

C++ 多线程同步之原子操作(atomic)实战

在这里插入图片描述

💡 学习目标:掌握 C++ 标准库中原子操作的使用方法,理解原子操作与互斥锁的区别,能够在轻量级同步场景中高效解决数据竞争问题。
💡 学习重点std::atomic 模板的常用接口、原子操作的特性、原子类型与普通类型的性能对比、原子操作的典型应用场景。

50.1 原子操作的引入背景

在 48 章我们学习了互斥锁,它通过阻塞线程的方式实现临界区保护。
但互斥锁存在上下文切换开销,在一些简单的同步场景中显得过于笨重。
比如对单个变量的自增、自减、赋值等操作,我们需要一种更轻量级的同步方案——原子操作。

⚠️ 注意事项:原子操作仅适用于单个变量的简单同步,无法替代互斥锁实现复杂临界区的保护。

举个例子,使用互斥锁保护变量自增:

#include<iostream>#include<thread>#include<mutex>usingnamespace std;int count =0; mutex mtx;voidincrement(){for(int i =0; i <100000;++i){ lock_guard<mutex>lock(mtx); count++;}}intmain(){ thread t1(increment); thread t2(increment); t1.join(); t2.join(); cout <<"最终 count 值:"<< count << endl;return0;}

这段代码虽然能保证线程安全,但每次加锁解锁都会带来额外开销。
原子操作可以在无锁的情况下实现同样的效果,且效率更高。

50.2 C++ 标准库中的原子操作

C++11 标准库在 <atomic> 头文件中提供了 std::atomic 模板类。
它可以将普通类型包装成原子类型,支持原子化的读、写、修改操作。

50.2.1 std::atomic 的核心特性

  1. 无锁同步:原子操作通过 CPU 指令级别的支持实现同步,不需要操作系统介入上下文切换。
  2. 不可分割性:原子操作的执行过程不会被其他线程打断,要么完全执行,要么完全不执行。
  3. 模板化设计std::atomic 是模板类,可以包装大多数基本数据类型,如 intboollong 等。

50.2.2 常用原子操作接口

std::atomic<int> 为例,常用接口如下:

  • 赋值与读取:支持直接用 = 赋值,用 load() 读取值,默认是顺序一致的内存序。
  • 自增自减fetch_add()(原子自增)、fetch_sub()(原子自减),返回操作前的值。
  • 复合操作exchange()(原子交换值)、compare_exchange_weak()(比较并交换)。
  • 简化操作:支持 ++--+=-= 等重载运算符,使用更便捷。

核心结论:原子操作的重载运算符(如 ++)是对 fetch_add() 的封装,使用起来和普通变量几乎一致。

50.3 原子操作实战:替代互斥锁解决简单同步问题

我们使用 std::atomic<int> 改造 50.1 节的例子,实现无锁同步:

#include<iostream>#include<thread>#include<atomic>usingnamespace std; atomic<int>count(0);// 定义原子类型变量voidincrement(){for(int i =0; i <100000;++i){ count++;// 原子自增操作,无需加锁}}intmain(){ thread t1(increment); thread t2(increment); t1.join(); t2.join(); cout <<"最终 count 值:"<< count << endl;return0;}

运行该程序,最终 count 的值稳定等于 200000
和互斥锁版本相比,这段代码不仅更简洁,而且执行效率更高。

50.3.1 原子操作的内存序(可选进阶知识点)

std::atomic 的操作可以指定内存序,用来平衡性能和同步强度。
常用的内存序有三种:

  1. memory_order_seq_cst:顺序一致内存序,最强的同步保证,也是默认值。
  2. memory_order_acquire/release:适用于生产者-消费者模型,保证操作的先后顺序。
  3. memory_order_relaxed:松散内存序,仅保证操作的原子性,不保证顺序,性能最优。

示例:使用松散内存序优化自增操作

voidincrement(){for(int i =0; i <100000;++i){ count.fetch_add(1, memory_order_relaxed);}}

在只需要保证原子性的场景下,松散内存序可以显著提升性能。

50.4 原子操作 vs 互斥锁:适用场景对比

特性原子操作互斥锁
同步粒度仅支持单个变量的原子操作支持任意复杂的临界区代码
性能开销低(CPU 指令级,无上下文切换)高(可能触发上下文切换)
使用复杂度低(类似普通变量,无需手动加解锁)高(需手动管理锁的生命周期)
死锁风险有(不当使用会导致死锁)

💡 选型技巧

  • 简单变量同步(如计数器、标志位)→ 优先使用原子操作
  • 复杂临界区(如多变量操作、函数调用)→ 必须使用互斥锁

50.5 实战案例 1:原子标志位实现线程退出控制

原子布尔类型 std::atomic<bool> 常用于实现线程的安全退出控制:

#include<iostream>#include<thread>#include<atomic>#include<chrono>usingnamespace std; atomic<bool>is_running(true);// 原子标志位voidworker(){while(is_running.load()){// 原子读取标志位 cout <<"线程正在运行..."<< endl; this_thread::sleep_for(chrono::milliseconds(500));} cout <<"线程安全退出"<< endl;}intmain(){ thread t(worker); this_thread::sleep_for(chrono::seconds(2)); is_running =false;// 原子赋值,通知线程退出 t.join(); cout <<"主线程结束"<< endl;return0;}

运行效果:线程运行 2 秒后,检测到标志位变为 false,安全退出。
这个场景用原子操作比互斥锁更轻量、更高效。

50.6 实战案例 2:原子计数器实现多线程任务统计

使用原子操作实现多线程任务完成情况的统计,无需加锁:

#include<iostream>#include<thread>#include<atomic>#include<vector>usingnamespace std;constint TASK_COUNT =10; atomic<int>completed_tasks(0);// 已完成任务数voidtask(int id){ cout <<"任务"<< id <<"开始执行"<< endl; this_thread::sleep_for(chrono::milliseconds(300)); completed_tasks++;// 原子自增,统计完成数 cout <<"任务"<< id <<"执行完毕"<< endl;}intmain(){ vector<thread> threads;for(int i =1; i <= TASK_COUNT;++i){ threads.emplace_back(task, i);}for(auto& t : threads){ t.join();} cout <<"所有任务执行完毕,总计完成:"<< completed_tasks <<"个"<< endl;return0;}

运行该程序,最终输出的完成任务数一定等于 10
这个案例充分体现了原子操作在简单统计场景下的优势。

50.7 原子操作的常见误区

  1. 忽略内存序的影响
    在多线程通信场景下,随意使用松散内存序可能导致程序行为异常。
    新手建议优先使用默认的顺序一致内存序,熟悉后再优化。
  2. 认为原子操作一定比互斥锁快
    在高竞争场景下,原子操作的自旋等待可能导致 CPU 占用过高。
    此时互斥锁的阻塞机制反而更高效。

误用原子操作保护复杂逻辑
原子操作只能保证单个操作的原子性,不能保护多个原子操作的组合。
例如:

// 错误示例:这两个原子操作的组合不是原子的if(count <100){ count++;}

这个逻辑可能被多个线程打断,需要互斥锁来保护。

50.8 本章小结

  1. 原子操作是轻量级同步方案,通过 CPU 指令级支持实现无锁同步,适用于单个变量的同步场景。
  2. std::atomic 是 C++ 标准库的原子类型模板,支持自增、自减、交换等常用原子操作。
  3. 原子操作和互斥锁各有适用场景:简单变量用原子操作,复杂临界区用互斥锁。
  4. 原子操作的内存序可以平衡性能和同步强度,新手建议先使用默认的顺序一致内存序。

Read more

【开源发布】MCP Document Reader:让你的 AI 助手真正读懂需求文档!

【个人主页:玄同765】 大语言模型(LLM)开发工程师|中国传媒大学·数字媒体技术(智能交互与游戏设计) 深耕领域:大语言模型开发 / RAG知识库 / AI Agent落地 / 模型微调 技术栈:Python / LangChain/RAG(Dify+Redis+Milvus)| SQL/NumPy | FastAPI+Docker ️ 工程能力:专注模型工程化部署、知识库构建与优化,擅长全流程解决方案        「让AI交互更智能,让技术落地更高效」 欢迎技术探讨/项目合作! 关注我,解锁大模型与智能交互的无限可能! 前言:为什么 AI 总是“读不动”你的文件? 【好消息】MCP Document Converter 已正式入驻 MCP 官方 Server 列表,

By Ne0inhk

0代码实战:基于 Coze 平台搭建全自动 AI 视频生成 Agent(附工作流源码思路)

💥 炸裂!用 Coze 工作流 + Agent 5分钟搞定 AI 视频?告别剪映,这才是创作者的“核武”! 摘要: 还在手动找素材、配字幕、调音色?你 Out 了!当 Sora 还在“画饼”时,聪明的开发者已经用 Coze(扣子) 搭建了全自动的 AI 视频生产流水线。本文将揭秘如何利用 Coze 的“工作流”和“插件”能力,打造一个能写脚本、能生图、能配音的 AI 视频制作 Agent,让你的工作量直接降低 90%! 一、 痛点:为什么传统的 AI 视频制作这么累? 现在的

By Ne0inhk
Adobe Illustrator Ai 2025下载安装保姆级教程(附安装包)

Adobe Illustrator Ai 2025下载安装保姆级教程(附安装包)

文章目录 * AI安装准备工作 * 下载安装包 * 我的使用小技巧(多年经验总结) 嘿,各位设计小伙伴!今天想跟大家分享一下我安装Adobe Illustrator 2025的全过程和一些心得体会。作为一名使用AI软件已经7年多的老用户,我经历过无数次的版本更新和重装,踩过不少坑,也总结出了一套行之有效的安装方法。希望我的经验能帮助到刚入门或者需要重新安装AI的朋友们! AI安装准备工作 在正式开始安装前,我总会做这几件事(血的教训总结出来的!): 1. 清理电脑环境:我会先关闭所有杀毒软件和防火墙。有一次我忘记关闭,结果安装到一半被杀毒软件拦截,白白浪费了半小时… 2. 检查磁盘空间:AI虽然本身不算特别大,但我习惯预留至少15GB的空间。因为使用过程中临时文件和缓存会占用不少空间,空间不足会导致软件运行卡顿(亲身体会,太难受了)。 3. 备份重要文件:虽然安装新软件理论上不会影响现有文件,但我还是养成了备份的好习惯。曾经因为一次系统崩溃丢失了一个重要客户的设计稿,那种心情简直糟糕透了! 下载安装包 本教程相关的代码以及资料相当大好几个G,放下面了只

By Ne0inhk

Flutter 组件 pathfinding 的鸿蒙化适配实战 - 驾驭极致拓扑寻踪大坝、实现 OpenHarmony 分布式端高性能 AI 寻路、迷宫拓扑与工业级路径导航核方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 pathfinding 的鸿蒙化适配实战 - 驾驭极致拓扑寻踪大坝、实现 OpenHarmony 分布式端高性能 AI 寻路、迷宫拓扑与工业级路径导航核方案 前言 在鸿蒙(OpenHarmony)生态的分布式工业巡检、高性能游戏开发或者是对空间计算有极其严苛要求的 0308 批次智能仓储应用中。“复杂环境下的路径最优解计算与实时障碍避让维度”是衡量整个系统智慧化程度的最终质量门禁。面对包含数万个节点的网格地图、海量动态变化的货架坐标、甚至是由于跨设备同步产生的 0308 批次拓扑逻辑海洋。如果仅仅依靠简单的“直线欧式距离”或者是干瘪的广度优先搜索(BFS)。不仅会导致在处理大型复杂地图时让系统如同在逻辑废墟中盲人摸象。更会因为计算耗时指数级爆炸,让移动端在进行路径导航时瞬间陷入死机盲区。 我们需要一种“逻辑先行、代价建模”的空间演算艺术。 pathfinding 是一套专注于无缝整合全球公认顶级算法 A*、Dijkstra 以及二叉堆

By Ne0inhk