C++中的atomic:原子

在多线程编程中,原子操作是一种重要的技术,可以确保在并发环境中进行无锁操作。C++11 引入了 std::atomic<>,提供了一种无锁的机制来操作变量,确保在并发环境中访问和修改变量时没有数据竞争。本文将介绍 std::atomic<> 的基本概念、使用场景以及实现原理,并提供一些示例代码来说明其用法。

什么是原子操作?

原子操作是指在多线程环境下,不可分割的操作,即在进行操作时不会被其他线程打断。原子操作是一种以单个事务来执行的操作,其他线程只能看到操作完成前或者完成后的资源状态,不存在中间状态可视。从底层来看,原子操作是一些硬件指令,其原子性由硬件保证,C++11 对原子操作进行了抽象,提供了统一的接口,避免使用时嵌入平台相关的代码来支持跨平台使用。

为什么需要 std::atomic<>

虽然 C++11 提供了其他高并发的 API,例如互斥锁(std::mutex)和条件变量(std::condition_variable),但这些 API 在某些情况下并不是最佳选择:

  • 性能开销:互斥锁的实现通常依赖于操作系统的系统调用,这可能带来较大的性能开销。相反,std::atomic<> 的操作通常是无锁的,直接使用底层硬件提供的原子操作,性能更高。
  • 避免死锁:使用互斥锁需要小心处理锁的获取和释放,否则容易导致死锁。std::atomic<> 不需要显式加锁和解锁,减少了编程复杂性。
  • 细粒度控制std::atomic<> 提供了对单个变量的细粒度控制,使得某些简单的并发场景可以更高效地实现。

使用 std::atomic<>

std::atomic<> 是一个模板类,可以实例化各种类型的原子操作。然而,并不是所有类型都可以实例化 std::atomic<> 模板。按照标准的说法,需要是 Trivially Copyable 的类型,简单来说就是满足以下三个条件:

  1. 连续的内存;
  2. 拷贝对象意味着按 bit 拷贝(memcpy);
  3. 没有虚函数。

用代码来表达则是自定义结构满足下面五个条件:

  • std::is_trivially_copyable<T>::value
  • std::is_copy_constructible<T>::value
  • std::is_move_constructible<T>::value
  • std::is_copy_assignable<T>::value
  • std::is_move_assignable<T>::value

示例代码

下面的代码演示了如何使用 std::atomic<> 进行各种原子操作:

#include <ostream>
#include "XLogger.h"
#include <atomic>
#include <sstream>
#include <type_traits>

class AtomicInfo {};

class q_atomic {
public:
    q_atomic() {
      std::atomic<int32_t> int32_atomic(100);
      int32_atomic++;
      int32_atomic--;
      ++int32_atomic;

      std::atomic<bool> bool_atomic(false);

      /**
       * 读取并修改被封装的值,exchange 会将 val 指定的值替换掉之前该原子对象封装的值,并返回之前该原子对象封装的值,
       * 整个过程是原子的(因此exchange 操作也称为 read-modify-write 操作)。
       */
      std::atomic<bool> atomicBool2 = bool_atomic.exchange(true, std::memory_order_relaxed);
      bool_atomic = true;


      std::atomic<AtomicInfo> info_atomic;

      // 这个错误信息指出 _Atomic 不能应用于 std::string 类型,因为 std::string 不是可平凡复制(trivially copyable)的类型。
      // C++ 中 std::atomic<> 只能用于可平凡复制的类型,例如基本数据类型(如 int、float)和某些自定义类型,但 std::string 不符合这一要求。
      // todo 为什么 std::string 不能直接用于 std::atomic<>?
      //  std::string 是一个复杂的类型,包含动态分配的内存,其复制操作涉及更多的资源管理,不能保证在并发环境下安全地进行无锁操作。
      //  使用锁 或 包装string std::atomic<std::shared_ptr<std::string>> atomicString(std::make_shared<std::string>("")); 规避
      //std::atomic<std::string> str_atomic;
    }

    void bring() {
      std::atomic<int32_t> atomicInt(100);
      /**
       * 修改被封装的值
       * void store(_Tp __d, memory_order __m = memory_order_seq_cst)
       * memory_order __m,指定内存序,操作的类别决定了内存次序所准许的取值。若我们没有把内存次序显式设定成上面的值,
       *    则默认采用最严格的内存次序,即std::memory_order_seq_cst
       */
      atomicInt.store(100, std::memory_order_seq_cst);

      /**
       * 读取被封装的值
       * _Tp load(memory_order __m = memory_order_seq_cst)
       */
      int32_t result = atomicInt.load();

      /**
        * 检查std::atomic对象是否为无锁实现。
        * 返回值:bool,如果当前平台支持无锁实现,返回true,否则返回false。
        *
        * 判断该 std::atomic 对象是否具备 lock-free 的特性。如果某个对象满足 lock-free 特性,在多个线程访问该对象时不会导致线程阻塞
        * 这是一个运行时的判断(C++17提供了编译时判断constexpr is_always_lock_free()),之所以会出现无锁不确定的情况主要是因为对齐alignment。
       */
      bool is_lock_free = atomicInt.is_lock_free();
      XLOG_INFO("is_lock_free: {0}", is_lock_free);//true

      /**
       * 设置原子对象的新值,并返回旧值。
       * 将 val 指定的值替换掉之前该原子对象封装的值,并返回之前该原子对象封装的值,整个过程是原子的(因此exchange 操作也称为 read-modify-write 操作)。
       */
      atomicInt.exchange(12, std::memory_order_seq_cst);

      /**
       * 原子地比较并交换值。与compare_exchange_weak不同,它在失败时不会进行多次尝试。
       * 比较并交换被封装的值与参数 expected 所指定的值是否相等,如果:
       *    相等,则用 val 替换原子对象的旧值
       *    不相等,则用原子对象的旧值替换 expected ,因此调用该函数之后,如果被该原子对象封装的值与参数 expected 所指定的值不相等,expected 中的内容就是原子对象的旧值。
       *
       * 如果值被交换,返回true,否则返回false
       *
       * Tp& __e,预期值的引用
       * _Tp __d,要设置的新值。
       * memory_order __s, 成功的内存序
       * memory_order __f, 失败的内存序
       * bool compare_exchange_strong(_Tp& __e, _Tp __d, memory_order __s, memory_order __f)
       */
      int32_t expected = 10;
      bool success_ = atomicInt.compare_exchange_strong(expected, 199,std::memory_order_seq_cst, std::memory_order_seq_cst);
      XLOG_INFO("compare_exchange_strong: {0}", success_);

      /**
       * 比较并交换(弱版本,可能会伪失败)。
       * 与compare_exchange_strong 不同, weak 版本的 compare-and-exchange 操作允许原子对象所封装的值与参数 expected 的物理内容相同,但却仍然返回 false,
       * 不过在某些需要循环操作的算法下这是可以接受的,并且在一些平台下 compare_exchange_weak 的性能更好 。如果 compare_exchange_weak 的判断确实发生了伪失败(spurious failures)——即使原子对象所封装的值与参数 expected 的物理内容相同,
       * 但判断操作的结果却为 false,compare_exchange_weak函数返回 false,并且参数 expected 的值不会改变。
       *
       * 对于某些不需要采用循环操作的算法而言, 通常采用compare_exchange_strong 更好
       *
       * 如果 atomicInt == expected,则 atomicInt = 20并返回true,否则返回false并将expected设为atomic_int的值
       */
      bool exchanged = atomicInt.compare_exchange_weak(expected, 20);
      XLOG_INFO("compare_exchange_weak: {0}", exchanged);

      /**
       * 原子加法,返回旧值。可选的内存顺序,默认为memory_order_seq_cst。
       */
      int add_ret = atomicInt.fetch_add(123);

      /**
       * 原子减法,返回旧值。可选的内存顺序,默认为memory_order_seq_cst。
       */
      int sub_ret = atomicInt.fetch_sub(100);

      //原子或操作,返回旧值。
      atomicInt.fetch_or(1);

      //原子与操作,返回旧值。
      //old_value = 0b1100, atomic_int = 0b1000
      int old_value = atomicInt.fetch_and(0b1010);

      //原子异或操作,返回旧值。
      atomicInt.fetch_xor(11);

      //等待直到atomic_int的值不等于10
      atomicInt.wait(10);
      atomicInt.notify_one();
      atomicInt.notify_all();//通知等待的所有线程。

      /**
       * 实际调用了 operator T() const, 将foo 强制转换成 int 类型,然后调用 operator=().
       * 与 load 功能类似,也是读取被封装的值,operator T() 是类型转换(type-cast)操作,
       * 默认的内存序是 std::memory_order_seq_cst,如果需要指定其他的内存序,应该使用 load() 函数。
       */
      std::atomic<int32_t> atomicInt2 = static_cast<int>(atomicInt);
    }

    void task1() {
      //编译时常量布尔值,用于检查类型 T 是否是可平凡复制的。一个类型是可平凡复制的,
      // 意味着它的复制操作(拷贝构造、拷贝赋值)都可以通过简单的内存复制(如 memcpy)完成,而不需要自定义的拷贝逻辑。
      bool copyable = std::is_trivially_copyable<std::string>::value;
      XLOG_INFO("std::string-->is_trivially_copyable: {0}", copyable);//false

      //编译时常量布尔值,用于检查类型 T 是否是可拷贝构造的。即,类型 T 是否可以通过拷贝构造函数创建新的对象。
      //用途: 在模板代码中判断类型是否支持拷贝构造,确保只有支持拷贝构造的类型才能被某些模板实例化。
      bool constructible = std::is_copy_constructible<std::string>::value;
      XLOG_INFO("std::string-->is_copy_constructible: {0}", constructible);//true

      bool is_copy_assignable=std::is_copy_assignable<int32_t>::value;
      XLOG_INFO("int32_t-->is_copy_assignable: {0}", is_copy_assignable);//true

      //编译时常量布尔值,用于检查类型 T 是否是可移动赋值的。即,类型 T 是否可以通过移动赋值运算符进行赋值操作。
      //用途: 在模板代码中判断类型是否支持移动赋值,从而优化代码性能,减少不必要的拷贝操作。
      bool move_assignable = std::is_move_assignable<int32_t>::value;
      XLOG_INFO("int32_t-->is_move_assignable: {0}", move_assignable);//true
      checkTypeTraits<int32_t>();
      checkTypeTraits<long>();
      checkTypeTraits<std::string>();
    }

    template <typename T>
    void checkTypeTraits() {
      std::cout << "Is trivially copyable: " << std::is_trivially_copyable<T>::value << std::endl;
      std::cout << "Is copy constructible: " << std::is_copy_constructible<T>::value << std::endl;
      std::cout << "Is move constructible: " << std::is_move_constructible<T>::value << std::endl;
      std::cout << "Is copy assignable: " << std::is_copy_assignable<T>::value << std::endl;
      std::cout << "Is move assignable: " << std::is_move_assignable<T>::value << std::endl;
    }

    ~q_atomic() {}

private:
};

注意事项

  • std::atomic<> 只能用于可平凡复制的类型,例如基本数据类型(如 intfloat)和某些自定义类型,但 std::string 不符合这一要求。std::string 是一个复杂的类型,包含动态分配的内存,其复制操作涉及更多的资源管理,不能保证在并发环境下安全地进行无锁操作。
  • 可以使用 std::atomic<std::shared_ptr<std::string>> 来处理 std::string,这是一种间接方式,确保线程安全。

总结

std::atomic<> 提供了一种高效、安全的方式来处理多线程环境中的共享数据。通过利用底层硬件的原子操作指令,可以避免使用互斥锁,从而提高性能,并减少死锁的风险。在编写高并发应用程序时,合理使用 std::atomic<> 可以大大简化代码并提高程序的稳定性和性能。

相关推荐

  1. C++atomic原子

    2024-07-22 18:32:01       14 阅读
  2. C语言实现atoi函数实现

    2024-07-22 18:32:01       30 阅读
  3. C++ 多线程 atomic

    2024-07-22 18:32:01       57 阅读
  4. 原子计数器缓冲区 Atomic Counter Buffers

    2024-07-22 18:32:01       37 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-22 18:32:01       52 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-22 18:32:01       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-22 18:32:01       45 阅读
  4. Python语言-面向对象

    2024-07-22 18:32:01       55 阅读

热门阅读

  1. Nacos 面试题及答案整理,最新面试题

    2024-07-22 18:32:01       20 阅读
  2. 【Unity】RPG2D龙城纷争(十五)特殊加成型要诀

    2024-07-22 18:32:01       16 阅读
  3. 软考高级第四版备考--第27天(项目工作绩效域)

    2024-07-22 18:32:01       17 阅读
  4. ETCD介绍以及Go语言中使用ETCD详解

    2024-07-22 18:32:01       18 阅读
  5. C语言:再探C语言指针

    2024-07-22 18:32:01       22 阅读
  6. 安卓开发使用seekBar改变ImageView中图片的色彩

    2024-07-22 18:32:01       15 阅读
  7. matlab中feval()的用法

    2024-07-22 18:32:01       15 阅读