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,提升项目的性能与安全性。

相关推荐
夏天的味道٥3 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
IT、木易4 小时前
大白话JavaScript实现一个函数,将字符串中的每个单词首字母大写。
开发语言·前端·javascript·ecmascript
Mr.NickJJ5 小时前
JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件
开发语言·javascript·react.js
Dream it possible!5 小时前
LeetCode 热题 100_字符串解码(71_394_中等_C++)(栈)
c++·算法·leetcode
Archer1946 小时前
C语言——链表
c语言·开发语言·链表
My Li.6 小时前
c++的介绍
开发语言·c++
功德+n6 小时前
Maven 使用指南:基础 + 进阶 + 高级用法
java·开发语言·maven
达斯维达的大眼睛6 小时前
qt小项目,简单的音乐播放器
开发语言·qt
面会菜.6 小时前
C语言(队列)
c语言·开发语言
香精煎鱼香翅捞饭7 小时前
java通用自研接口限流组件
java·开发语言