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

深入理解C++11原子操作:从内存模型到无锁编程

文章目录

    • 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.lryc.cn/news/579090.html

相关文章:

  • SpringCloud系列(47)--SpringCloud Bus实现动态刷新定点通知
  • 04-动态规划
  • 数学建模_微分方程
  • 内存架构的十字路口:深入解析统一内存访问(UMA)与非一致内存访问(NUMA)
  • 虚拟机知识点-Vagrant 在通过 VirtualBox 启动 CentOS 虚拟机时失败-VERR_NEM_VM_CREATE_FAILED
  • 从0开始学习R语言--Day36--空间杜宾模型
  • maven仓库
  • WSL2 + Docker Desktop 环境中查看本地镜像
  • 【Vue入门学习笔记】Vue核心语法
  • CentOS 卸载docker
  • 移动conda虚拟环境的安装目录
  • mongo常用命令
  • odoo17 警示: selection attribute will be ignored as the field is related
  • Node.js-http模块
  • Day04:玩转标准库中的数据处理与日志记录
  • Chart.js 安装使用教程
  • 基于SpringBoot和Leaflet的区域冲突可视化系统(2025企业级实战方案)
  • VC Spyglass:工具简介
  • React Native 开发环境搭建--window--android
  • 24年京东秋季笔试题
  • CSS外边距合并(塌陷)全解析:原理、场景与解决方案
  • flutter更改第三方库pub get的缓存目录;更改.gradle文件夹存放目录
  • 告别告警风暴:深入理解 Prometheus Alertmanager 的智能告警策略
  • 为什么星敏感器(Star Tracker)需要时间同步?—— 从原理到应用的全解析
  • 1-RuoYi框架配置与启动
  • 整流电路Multisim电路仿真实验汇总——硬件工程师笔记
  • qml实现 裁剪进度条
  • 使用案例 - 根据nuscenes-devkit工具读取nuscnes数据集
  • Active-Prompt:让AI更智能地学习推理的革命性技术
  • Ubuntu-18.04-bionic 的apt的/etc/apt/sources.list 更换国内镜像软件源 笔记250702