Modern C++ std::atomic简介

文章目录

    • [为什么选择 std::atomic?](#为什么选择 std::atomic?)
    • [std::atomic 的主要特性](#std::atomic 的主要特性)
      • [1. 支持多种操作](#1. 支持多种操作)
      • [2. 灵活的内存序](#2. 灵活的内存序)
      • [3. 支持多种类型](#3. 支持多种类型)
    • [实践篇:std::atomic 的使用场景](#实践篇:std::atomic 的使用场景)
      • 环境要求
      • [场景 1:如何用 `std::atomic` 实现线程安全计数器](#场景 1:如何用 std::atomic 实现线程安全计数器)
        • 示例需求
        • [使用 `std::atomic` 的高效实现](#使用 std::atomic 的高效实现)
      • [场景 2:多线程竞争同一任务](#场景 2:多线程竞争同一任务)
      • [场景 3: 协调执行阶段](#场景 3: 协调执行阶段)
    • 高级篇:内存序模型
    • 总结

在现代高性能多线程编程中,如何高效、安全地处理共享数据是一个关键问题。std::atomic 作为 C++ 标准库提供的一种无锁线程安全工具,因其性能优越和易用性而备受推崇。本文将深入探讨 std::atomic 的特性、使用方法及其在实际开发中的应用场景,帮助读者全面掌握这一工具。


为什么选择 std::atomic?

在多线程环境下,数据共享和修改可能导致竞争条件。传统的 std::mutex 可以通过加锁实现线程安全,但由于其重量级的锁机制,可能带来显著的性能开销。

相比之下,std::atomic 具有以下显著优势:

  1. 线程安全性:所有操作均为原子性,避免数据竞争。
  2. 无锁机制:通过硬件支持的原子指令,消除了线程上下文切换的开销。
  3. 内存序模型:灵活的内存序控制,满足不同场景下的性能与一致性需求。

适用场景包括:

  • 简单变量的多线程读写,如计数器、标志位。
  • 替代频繁加锁的场景,以优化性能。

std::atomic 的主要特性

1. 支持多种操作

  • 基本操作loadstore 实现共享变量的读取和写入。
  • 数学运算fetch_addfetch_sub 提供原子加减功能。
  • 比较并交换compare_exchange_weakcompare_exchange_strong 用于实现条件更新。
  • 高效等待与通知 :通过 waitnotify_onenotify_all 实现线程间的高效协作。

2. 灵活的内存序

提供从 memory_order_relaxedmemory_order_seq_cst 的多种内存序控制,可根据场景需求在性能与一致性之间灵活取舍。

3. 支持多种类型

适用于基础类型(如 intbool),也可通过特化支持自定义类型。


实践篇:std::atomic 的使用场景

环境要求

本文中的代码需要编译器支持 C++20 标准. 本文在

  1. GCC 13.2 上面测试通过
  2. Clang 18.1 上面测试通过

场景 1:如何用 std::atomic 实现线程安全计数器

示例需求

实现一个计数器,支持以下操作:

  • Increase():将计数器加 1,并返回更新后的值。
  • IncreaseBy(int):增加指定值,并返回更新后的值。
  • DecreaseBy(int):减少指定值,并返回更新后的值。
  • Get():返回当前计数器的值。
  • Reset():将计数器重置为初始值。
使用 std::atomic 的高效实现
cpp 复制代码
#include <atomic>
#include <iostream>

class Counter {
 public:
  Counter(int id) : counter_(id) {}

  void Reset(int cnt) { counter_.store(cnt); }

  int Get() { return counter_.load(); }

  int Increase() { return counter_++; }

  int IncreaseBy(int cnt) { return counter_.fetch_add(cnt) + cnt; }

  int DecreaseBy(int cnt) { return counter_.fetch_sub(cnt) - cnt; }

 private:
  std::atomic<int> counter_;
};

int main() {
  Counter counter(0);
  counter.Increase();
  counter.IncreaseBy(10);
  counter.DecreaseBy(5);
  std::cout << "Counter: " << counter.Get() << std::endl;

  return 0;
}

在 Compiler Explorer 上查看.

场景 2:多线程竞争同一任务

示例需求

有一些特定的任务需要有一个线程来执行,并且需要保证只有一个线程能够成功。这时,我们可以使用 std::atomic 来实现。

示例代码
cpp 复制代码
#include <atomic>
#include <chrono>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

std::atomic<unsigned long int> leader_id(-1);

void try_to_be_leader() {
  // 生成一个 100 到 1000 毫秒之间的随机睡眠时间
  std::random_device rd;
  std::mt19937 gen(rd());
  std::uniform_int_distribution<> dis(100, 1000);
  int sleep_duration = dis(gen);

  // 休眠随机时间, 模拟线程执行任务
  std::this_thread::sleep_for(std::chrono::milliseconds(sleep_duration));

  auto thread_id = std::this_thread::get_id();
  unsigned long int expected = -1;
  // 尝试成为 leader
  if (leader_id.compare_exchange_strong(
          expected, std::hash<std::thread::id>{}(thread_id))) {
    std::cout << "Thread " << thread_id << " became the leader." << std::endl;
  } else {
    std::cout << "Thread " << thread_id << " failed." << std::endl;
  }
}

int main() {
  constexpr int kNumThreads = 10;
  std::vector<std::jthread> threads;

  for (int i = 0; i < kNumThreads; ++i) {
    threads.emplace_back(try_to_be_leader);
  }

  std::cout << "Leader is: " << leader_id.load() << std::endl;
  return 0;
}

在 Compiler Explorer 上查看.

运行结果

txt 复制代码
Thread 127785328707264 became the leader.
Thread 127785276278464 failed.
Thread 127785297249984 failed.
Thread 127785307735744 failed.
Thread 127785286764224 failed.
Thread 127785318221504 failed.
Thread 127785339193024 failed.
Thread 127785360164544 failed.
Thread 127785370650304 failed.
Thread 127785349678784 failed.
Leader is: 6834516529000622202

说明compare_exchange_strong 确保只有一个线程能成功设置 leader_id,实现了多线程下的安全竞争。

场景 3: 协调执行阶段

示例需求

在多线程编程中,不同线程之间需要协调工作,常常需要使用标志位来进行通信。例如,一个线程在数据准备完毕后需要通知其他线程开始工作。这时,我们可以使用 std::atomic<bool> 来实现高效的标志位。

示例代码
cpp 复制代码
#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

int main() {
  std::atomic<bool> ready{false};

  std::jthread producer([&]() {
    std::cout << "Producer: Preparing data...\n";
    // 模拟数据准备
    std::this_thread::sleep_for(std::chrono::seconds(2));

    // 数据准备完毕
    ready.store(true);
    // 通知等待的线程
    ready.notify_one();

    std::cout << "Producer: Data ready.\n";
  });

  std::jthread consumer([&]() {
    // 等待直到 ready 为 true
    ready.wait(false);
    std::cout << "Consumer: Data received.\n";
  });

  return 0;
}

在 Compiler Explorer 上查看.

说明 :通过 waitnotify_all,可以高效地实现生产者-消费者模式,避免频繁轮询带来的性能浪费。


高级篇:内存序模型

std::atomic 支持以下内存序模型:

  1. memory_order_relaxed:不保证顺序,性能最高。
  2. memory_order_acquire:保证后续读取可见之前的写入。
  3. memory_order_release:保证之前的写入对其他线程可见。
  4. memory_order_acq_rel:结合获取和释放语义。
  5. memory_order_seq_cst:全局顺序一致性,最强保证。

应用场景

  • 高性能优化 :计数器的无序递增使用 memory_order_relaxed
  • 同步机制 :锁实现中使用 memory_order_acquire/release
  • 复杂算法:无锁队列、信号处理中平衡性能与一致性。

总结

通过合理使用 std::atomic,可以在性能与一致性之间找到最佳平衡。尽管其适用范围主要限于简单变量的同步,但其高效、灵活的特性使其成为现代 C++ 多线程编程的重要工具。希望本文的深入探讨能够帮助你更好地掌握并应用 std::atomic,提升项目的性能与安全性。

相关推荐
Milk夜雨12 分钟前
头歌实训数据结构与算法-二叉树及其应用(第9关:二叉树的顺序存储及基本操作)
开发语言·数据结构·数据库·c++·算法
xing.yu.CTF14 分钟前
python的urllib模块和http模块
开发语言·python·http
Want59541 分钟前
Python跨年烟花
开发语言·python·pygame
百年孤独_41 分钟前
Python:爬虫基础《爬取红楼梦》
开发语言·爬虫·python
_半夏曲44 分钟前
工厂+策略模式之最佳实践(疾病报卡维护模块API设计)
java·开发语言·设计模式
深耕AI1 小时前
使用MFC编写一个paddleclas预测软件
c++·mfc·图像分类·paddleclas
yqcoder1 小时前
html 中前缀的 data-开头的元素属性是什么
开发语言·前端·javascript
ChoSeitaku1 小时前
No.2十六届蓝桥杯备战|练习题4道|数据类型|字符型|整型|浮点型|布尔型|signed|unsigned(C++)
java·c++·算法
zwh12984540601 小时前
《C++设计模式》策略模式
c++·设计模式·策略模式
沉默的八哥1 小时前
Python学习路线
开发语言·python·学习