文章目录

    • C++并发编程的新纪元
    • 内存模型基础:可见性与有序性
      • 数据竞争的根源
      • happens-before关系
      • memory_order枚举详解
        • 1. memory_order_relaxed
        • 2. memory_order_acquire/memory_order_release
        • 3. memory_order_seq_cst
    • 原子操作详解
      • std::atomic模板
      • 核心原子操作
        • 1. 读取与存储
        • 2. 交换操作
        • 3. 增减操作
        • 4. 比较并交换(CAS)
      • atomic_flag
      • 初始化注意事项
    • 实战案例
      • 案例1:原子计数器 vs 互斥量计数器
      • 案例2:基于CAS的无锁栈
      • 案例3:原子操作实现简单的读写锁
    • 陷阱与最佳实践
      • 内存序误用导致的隐蔽bug
      • lock-free的误区
      • ABA问题
      • 何时选择原子操作
    • 总结

最近在写一个服务程序,多线程下的各种操作相比于单线程而言,需要考虑的细节是幂级提升呀!同时由于对多线程的一些函数使用不够熟悉,原理不清楚,导致自己很难写服务端!

C++并发编程的新纪元

在C++11之前,编写跨平台的多线程程序意味着要面对POSIX线程与Windows API的差异,甚至同一平台下不同编译器的行为不一致。2011年标准的发布彻底改变了这一局面,其中并发支持库(Concurrency support library)的引入标志着C++正式进入多线程时代。本文将聚焦于该库的核心组件之一——原子操作(Atomic operations),探讨其底层原理、实际应用及避坑指南。

原子操作作为无锁编程的基础,为高性能并发提供了可能,但也因其对内存模型的依赖而成为最容易误用的特性之一。与互斥量(如std::mutex)通过阻塞线程实现同步不同,原子操作通过硬件级别的指令保证操作的不可分割性,从而避免了线程上下文切换的开销。然而,这种性能优势是以复杂性为代价的——开发者必须深入理解CPU内存模型和编译器优化才能正确使用。

内存模型基础:可见性与有序性

数据竞争的根源

多线程环境下,数据竞争(Data Race)是最常见的bug来源。考虑以下代码:

int counter = 0;void increment() {for (int i = 0; i < 100000; ++i) {counter++; // 非原子操作,存在数据竞争}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Counter: " << counter << std::endl; // 结果可能小于200000return 0;
}

这段代码看似简单,却可能输出小于200000的结果。原因在于counter++并非原子操作,它包含三个步骤:读取当前值、加1、写回新值。当两个线程同时执行时,可能出现"读-读-写-写"的情况,导致其中一个增量操作被覆盖。

更深层次的原因在于现代CPU的优化机制:

  • 乱序执行:CPU为提高效率可能调整指令执行顺序
  • 缓存优化:每个CPU核心有独立缓存,变量修改可能暂存于缓存而非立即写入主存
  • 编译器优化:编译器可能对代码进行重排,导致实际执行顺序与源码顺序不同

happens-before关系

C++11引入了"happens-before"关系来定义操作间的可见性。如果操作A happens-before操作B,则A的结果对B可见。关键规则包括:

  • 同一线程内,按源码顺序执行的操作存在happens-before关系
  • 解锁操作happens-before后续的加锁操作
  • 原子操作的内存序约束可建立happens-before关系

memory_order枚举详解

C++11定义了六种内存序(memory_order),但实际开发中常用的有三种:

1. memory_order_relaxed

仅保证操作本身的原子性,不提供任何同步或顺序约束。适用于纯计数器等场景:

std::atomic<int> counter(0);
counter.fetch_add(1, std::memory_order_relaxed); // 仅保证计数正确,不影响其他操作顺序
2. memory_order_acquire/memory_order_release
  • release:当前线程的所有写操作在其他线程对同一原子变量的acquire操作前可见
  • acquire:可见所有在release操作前的写操作

典型应用是生产者-消费者模型:

std::atomic<bool> data_ready(false);
int shared_data;// 生产者线程
void producer() {shared_data = 42; // 1. 写入数据data_ready.store(true, std::memory_order_release); // 2. 发布信号
}// 消费者线程
void consumer() {while (!data_ready.load(std::memory_order_acquire)); // 3. 获取信号std::cout << shared_data; // 4. 安全读取数据,保证看到42
}
3. memory_order_seq_cst

最强的内存序,保证所有线程看到的操作顺序一致,如同在单个全局序列中执行。但性能开销最大,仅在需要全局同步时使用:

std::atomic<int> seq_cst_var(0);
seq_cst_var.store(1, std::memory_order_seq_cst); // 全局可见的存储操作

原子操作详解

std::atomic模板

std::atomic是一个模板类,支持基本类型(bool、char、int、long、指针等)的原子操作。对于用户自定义类型,需满足可平凡复制(Trivially Copyable)要求。

std::atomic<int> a(0);          // 整数原子变量
std::atomic<bool> flag(false);  // 布尔原子变量
std::atomic<MyStruct*> ptr(nullptr); // 指针原子变量

可通过atomic_is_lock_free检查操作是否真正无锁:

std::cout << std::boolalpha;
std::cout << "int is lock-free: " << std::atomic<int>{}.is_lock_free() << std::endl;
std::cout << "double is lock-free: " << std::atomic<double>{}.is_lock_free() << std::endl;

注意:在某些平台上,double类型的原子操作可能不是无锁的,会内部使用互斥量。

核心原子操作

1. 读取与存储
std::atomic<int> x(0);
int a = x.load(std::memory_order_relaxed); // 读取
x.store(5, std::memory_order_relaxed);     // 存储
x = 10; // 隐式使用memory_order_seq_cst,不推荐
2. 交换操作
std::atomic<int> x(5);
int old_val = x.exchange(10); // 原子交换,返回旧值(5)
3. 增减操作
std::atomic<int> count(0);
count.fetch_add(1); // 原子加1,返回旧值
count.fetch_sub(1); // 原子减1,返回旧值
count += 1; // 隐式使用memory_order_seq_cst
4. 比较并交换(CAS)

CAS是无锁编程的基石,操作逻辑为:“如果当前值等于预期值,则替换为新值,否则不做修改”。有强弱两个版本:

std::atomic<int> val(10);
int expected = 10;// 强版本:保证在val != expected时返回false
bool success = val.compare_exchange_strong(expected, 20);// 弱版本:可能伪失败(即使val == expected也可能返回false),需配合循环使用
do {expected = val.load();// 计算新值
} while (!val.compare_exchange_weak(expected, new_value));

弱版本在某些CPU架构上性能更好,适合循环场景;强版本适合单次尝试。

atomic_flag

std::atomic_flag是C++11中唯一保证lock-free的原子类型,仅支持test_and_set和clear操作:

std::atomic_flag flag = ATOMIC_FLAG_INIT; // 必须用此宏初始化// 实现简单自旋锁
class SpinLock {
private:std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:void lock() {while (flag.test_and_set(std::memory_order_acquire));}void unlock() {flag.clear(std::memory_order_release);}
};

初始化注意事项

C++11中原子变量的初始化需特别注意:

// 正确初始化方式
std::atomic<int> a{0};                  // C++11起支持
std::atomic<int> b(ATOMIC_VAR_INIT(0)); // 兼容C风格宏
std::atomic<int> c;
atomic_init(&c, 0);                     // 动态初始化// 错误方式
std::atomic<int> d = 0; // 拷贝初始化被禁用

实战案例

案例1:原子计数器 vs 互斥量计数器

对比原子操作与互斥量在多线程计数场景下的性能:

#include <atomic>
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>const int THREADS = 10;
const int ITERATIONS = 1000000;// 原子计数器
void atomic_counter_test() {std::atomic<int> counter(0);auto start = std::chrono::high_resolution_clock::now();std::vector<std::thread> threads;for (int i = 0; i < THREADS; ++i) {threads.emplace_back([&]() {for (int j = 0; j < ITERATIONS; ++j) {counter.fetch_add(1, std::memory_order_relaxed);}});}for (auto& t : threads) t.join();auto end = std::chrono::high_resolution_clock::now();std::cout << "Atomic counter: " << counter << " in "<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()<< "ms" << std::endl;
}// 互斥量计数器
void mutex_counter_test() {int counter = 0;std::mutex mtx;auto start = std::chrono::high_resolution_clock::now();std::vector<std::thread> threads;for (int i = 0; i < THREADS; ++i) {threads.emplace_back([&]() {for (int j = 0; j < ITERATIONS; ++j) {std::lock_guard<std::mutex> lock(mtx);counter++;}});}for (auto& t : threads) t.join();auto end = std::chrono::high_resolution_clock::now();std::cout << "Mutex counter: " << counter << " in "<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()<< "ms" << std::endl;
}int main() {atomic_counter_test();mutex_counter_test();return 0;
}

在笔者的4核CPU上,原子计数器通常比互斥量快3-5倍,且线程数越多差距越明显。

案例2:基于CAS的无锁栈

#include <atomic>
#include <iostream>template<typename T>
class LockFreeStack {
private:struct Node {T data;Node* next;Node(const T& data) : data(data), next(nullptr) {}};std::atomic<Node*> head;public:LockFreeStack() : head(nullptr) {}// 禁止拷贝构造和赋值LockFreeStack(const LockFreeStack&) = delete;LockFreeStack& operator=(const LockFreeStack&) = delete;~LockFreeStack() {while (Node* old_head = head.load()) {head.store(old_head->next);delete old_head;}}void push(const T& data) {Node* new_node = new Node(data);new_node->next = head.load(std::memory_order_relaxed);// 使用弱CAS循环处理可能的伪失败while (!head.compare_exchange_weak(new_node->next, new_node,std::memory_order_release,  // 成功时的内存序std::memory_order_relaxed)) // 失败时的内存序{}}bool pop(T& result) {Node* old_head = head.load(std::memory_order_relaxed);// 循环直到CAS成功或栈为空while (old_head && !head.compare_exchange_weak(old_head, old_head->next,std::memory_order_acquire,  // 成功时的内存序std::memory_order_relaxed)) // 失败时的内存序{}if (!old_head) return false;result = old_head->data;delete old_head;return true;}bool empty() const {return head.load(std::memory_order_relaxed) == nullptr;}
};int main() {LockFreeStack<int> stack;// 多线程pushstd::thread t1([&]() {for (int i = 0; i < 1000; ++i) {stack.push(i);}});// 多线程popstd::thread t2([&]() {int val;for (int i = 0; i < 500; ++i) {while (!stack.pop(val));std::cout << "Popped: " << val << std::endl;}});t1.join();t2.join();return 0;
}

案例3:原子操作实现简单的读写锁

#include <atomic>
#include <thread>class ReadWriteLock {
private:std::atomic<int> readers;std::atomic<bool> writer;public:ReadWriteLock() : readers(0), writer(false) {}void read_lock() {// 自旋等待写锁释放while (writer.load(std::memory_order_acquire)) {std::this_thread::yield();}readers.fetch_add(1, std::memory_order_relaxed);}void read_unlock() {readers.fetch_sub(1, std::memory_order_relaxed);}void write_lock() {bool expected = false;// 自旋等待写锁并尝试获取while (!writer.compare_exchange_weak(expected, true, std::memory_order_acquire, std::memory_order_relaxed)) {expected = false;std::this_thread::yield();}// 等待所有读者完成while (readers.load(std::memory_order_relaxed) > 0) {std::this_thread::yield();}}void write_unlock() {writer.store(false, std::memory_order_release);}
};

陷阱与最佳实践

内存序误用导致的隐蔽bug

最常见的错误是过度使用memory_order_relaxed。例如:

// 错误示例
std::atomic<bool> ready(false);
int data = 0;void producer() {data = 42;ready.store(true, std::memory_order_relaxed); // 错误:应使用release
}void consumer() {while (!ready.load(std::memory_order_relaxed)); // 错误:应使用acquireassert(data == 42); // 可能失败!
}

由于使用了relaxed内存序,编译器可能重排指令,导致data的写入在ready之后执行,消费者可能看到ready为true但data仍为0。

lock-free的误区

并非所有原子操作都是lock-free的。例如:

std::atomic<long double> ld; // 通常不是lock-free的

应始终使用is_lock_free()检查:

if (!std::atomic<MyType>{}.is_lock_free()) {// 回退到互斥量实现
}

ABA问题

CAS操作可能面临ABA问题:

std::atomic<Node*> ptr;// 线程1
Node* A = ptr.load();
// 线程2修改ptr从A到B再到A
// 线程1执行CAS,虽然ptr仍为A,但A可能已被修改
ptr.compare_exchange_strong(A, new_node);

解决方案是引入版本号:

struct TaggedPtr {Node* ptr;uint64_t version;
};
std::atomic<TaggedPtr> tagged_ptr;

何时选择原子操作

  • 适用场景:简单计数器、标志位、无锁数据结构
  • 不适用场景:复杂状态转换、需要多操作原子性(此时应使用互斥量)

经验法则:优先使用高级同步原语(如std::mutex、std::condition_variable),仅在性能关键路径且操作简单时才考虑原子操作。

总结

C++11原子操作为并发编程提供了强大的工具,但也要求开发者深入理解内存模型。正确使用原子操作可以显著提升性能,但错误使用会导致难以调试的并发bug。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/87277.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/87277.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/87277.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DQL-1-基础查询

基础查询 DQL-1-基础查询 基础查询DQL - 介绍DQL - 语法DQL - 基本查询案例 DQL - 介绍 SQL 英文全称是 Data Query Language, 数据查询语言, 用来查询数据库中表的记录 查询关键字: SELECT DQL - 语法 SELECT 字段列表FROM 表名列表WHERE条件列表GROUP BY分组字段列表HAVI…

Prompt 精通之路(七)- 你的终极 AI 宝典:Prompt 精通之路系列汇总

你的终极 AI 宝典&#xff1a;Prompt 精通之路系列汇总 标签&#xff1a; #Prompt指南 #AI学习资源 #速查手册 #ChatGPT #系列总结 &#x1f680; Prompt 精通之路&#xff1a;系列文章导航 第一篇&#xff1a;AI 时代的新语言&#xff1a;到底什么是 Prompt&#xff1f;为什么…

P27:RNN实现阿尔茨海默病诊断

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 一、过程解读 PyTorch 实战&#xff1a;阿尔茨海默病数据预测模型 今天&#xff0c;我将带大家一起探索一个基于 PyTorch 的深度学习小项目——利用 RNN 模…

HakcMyVM-Arroutada

信息搜集 主机发现 ┌──(kali㉿kali)-[~] └─$ nmap -sn 192.168.21.0/24 Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-01 07:13 EDT Nmap scan report for 192.168.21.11 Host is up (0.00062s latency). MAC Address: 08:00:27:4E:CC:FB (PCS Systemtechnik/Or…

TEXT Submitting Solutions

前言 USACO 训练项目配备了一个自动评分系统&#xff0c;用于批改你的作业题目。你可以直接在题目页面提交你的程序&#xff1b;系统会对程序进行编译和评分&#xff0c;几秒钟内就能将结果反馈给你。 支持的语言有 C、C&#xff08;含 C11 和 C14&#xff09;、PASCAL、Pyth…

Reactor 瞬态错误

在响应式编程中&#xff0c;retryWhen 操作符通过 RetrySignal 接口提供了对重试行为的精细控制&#xff0c;特别是在处理 瞬态错误&#xff08;transient errors&#xff09; 时。瞬态错误是指那些在一段时间内发生&#xff0c;但随后会自行恢复的错误&#xff0c;例如网络请求…

基于 SpringBoot+Vue.js+ElementUI 的小型超市商品管理系统设计与实现7000字论文设计

摘要 本论文设计并实现了一个基于 SpringBoot、Vue.js 和 ElementUI 的小型超市商品管理系统。该系统旨在为小型超市提供一个高效、便捷的商品管理解决方案&#xff0c;实现商品信息的录入、查询、修改、删除等功能&#xff0c;同时支持库存管理、销售统计等业务需求。论文首先…

Kerberos 认证协议解析

文章目录 概述核心概念认证流程阶段一&#xff1a;Client -> AS&#xff0c;获取 TGT阶段二&#xff1a;Client -> TGS&#xff0c;获取服务票据阶段三&#xff1a;Client -> Server&#xff0c;请求服务 核心安全机制优缺点分析优势局限性 实践与排错关键配置 (krb5.…

【设计模式07】适配器

前言 实现目标&#xff0c;组合源&#xff0c;写个适配方法&#xff0c;适用于没办法改变源&#xff0c;但又想实现目标类。我暂时还没使用到过&#xff0c;但感觉用处还是蛮大的 UML类图 代码示例 package com.sw.learn.pattern.C_structre.a_adapter;public class Main {//…

SPI、I2C和UART三种串行通信协议的--------简单总结

目录 一、3种协议的对比二、典型应用场景三、选型建议 以下是SPI、I2C和UART三种串行通信协议的对比分析及适用场景总结&#xff1a; 一、3种协议的对比 . 对比其他接口 特性ICSPIUART信号线数量2&#xff08;SCL SDA&#xff09;4&#xff08;SCK MOSI MISO SS/CS&…

VUE admin-element 后台管理系统三级菜单实现缓存

VUE admin-element 后台管理系统三级菜单实现缓存 框架无法直接实现三级菜单页面缓存&#xff0c;原因是由于直接缓存时没有把上级路由文件名称缓存进去&#xff0c;所以在框架基础上参考部分文章进行了一些改造 菜单文件&#xff0c;三级菜单引用文件路径修改&#xff0c;在…

【笔记】Windows 安装 Gemini CLI

2025 年 07 月 02 日 Windows 安装 Gemini CLI google-gemini/gemini-cli&#xff1a;一个开源的 AI 代理&#xff0c;可将 Gemini 的强大功能直接引入您的终端。 一、前置条件 系统要求&#xff1a;Windows 7 及以上版本。 Node.js 环境&#xff1a;Gemini CLI 基于 Node.js …

transformers==4.42.0会有一个BUG

transformers4.42.0版本下&#xff0c;自动安装模型时出现一个BUG&#xff08;自动从Hugging Faces上下载&#xff09;。 2025-07-02 14:07:08,641 - __main__ - ERROR - 模型加载失败: Failed to import transformers.models.llama.tokenization_llama_fast because of the f…

Spring-解决IDEA中无法创建JDK17一下的SpringBoot项目

目录 一.直接创建 二.修改Server URL为https://start.aliyun.com 一.直接创建 目前如果使用https://start.spring.io&#xff08;Spring官方源&#xff09;,已经没有办法直接创建JDK17一下的项目了&#xff1a; 如果想要创建JDK8的项目&#xff0c;可以先通…

人工智能-基础篇-13-基础应用篇-2~~模型项目开发流程--从0到1创建类似DeepSeek语言模型,应该怎么做?

1、前期准备 1、明确目标与需求分析 应用场景定义&#xff1a;首先需要明确你的模型将用于哪些场景&#xff0c;比如对话系统、文本生成、代码辅助等。性能指标设定&#xff1a;确定关键性能指标(KPI)&#xff0c;如准确率、响应时间、支持的语言种类等。 2、组建团队 机器…

本周沪铝想法

核心逻辑&#xff1a;低库存支撑与淡季需求疲软博弈&#xff0c;宏观情绪助推高位震荡 一、成本下移 VS 价格韧性​ 成本端与价格表现呈现出不同态势。成本端方面&#xff0c;氧化铝现货价格在本周持续下跌&#xff0c;山东地区均价降至 3090 元 / 吨&#xff0c;环比下降 1.…

【网络】SSL/TLS介绍

一、SSL/TLS 概述 SSL&#xff08;Secure Socket Layer&#xff09; &#xff1a; 最初由网景&#xff08;Netscape&#xff09;开发&#xff0c;用于在客户端和服务器之间建立安全的加密连接&#xff0c;防止数据被窃取或篡改。后来逐步演进&#xff0c;最终被 TLS 取代。 TL…

TLF35584

13、SPI串行外设接口 13.1 介绍 主要功能 SPI 总线是⼀种以全双工模式运行的同步串行数据链路。TLF35584 在从机模式下进行通信&#xff0c;其中主机(μC)启动数据帧。TLF35584应该通过专用片选线进行寻址。这允许其他从设备连接到SPI总线。 数据传输 开始通信&#xff0c;μ…

word中如何保存高清图片,并保存为高质量的pdf文件(图像不失真)

word中如何保存高清图片 打开word,选择&#xff0c;选项&#xff0c;高级选项&#xff0c;选择不压缩文件中的图像并保持分辨率高保真 将word保存为高质量的pdf文件 不用另存为或者导出 选择文件&#xff0c;选择打印&#xff1a; 选择中间都打印出pdf即可。 然后再选择打印…

Day03_C语言IO进程线程

01.思维导图 02.创建一个进程扇 #include <25051head.h> int main(int argc, const char *argv[]) {pid_t pid;int i;for(i0;i<4;i){pidfork();if(pid0){//printf("子进程:pid[%d]\n",pid);printf("子进程%d:子进程pid[%d],父进程pid[%d]\n",i1,g…