多线程-原子变量

144 阅读4分钟

一、无锁机制

无锁机制的定义是:

多线程环境下,至少有一个线程能在有限步内完成操作(保证系统整体不饿死) ,即线程不会陷入互相等待的“死锁”中。

换句话说:

  • 线程之间不使用传统的互斥锁(mutex)。
  • 线程可能会自旋重试,但不会被阻塞挂起
  • 某个线程可能反复失败 CAS,但整体系统依然“在进展”。

二、atomic相关API

  1. 条件原子交换 (Compare-and-Exchange, CAS) 这是所有无锁编程的基石,也是最强大、最核心的操作。

    • compare_exchange_weak(expected, desired)

    • compare_exchange_strong(expected, desired)

      • 作用:原子地比较原子变量的当前值与 expected(期望值)。

        • 如果相等:就用 desired(目标值)替换当前值,并返回 true
        • 如果不相等:不做任何修改,但会用原子变量的当前值去更新 expected(这是关键!),并返回 false
      • weak vs. strong 的区别:

        • strong:可靠。如果它返回 false,你可以百分百确定是因为值不相等。
        • weak可能“伪失败” 。它在值相等的情况下,也有可能会偶然地交换失败并返回 false
      • 为什么需要 weak:在某些硬件平台上,weak 版本的实现可以被编译成更高效的指令。因为它可能伪失败,所以它必须被用在一个循环里。我们之前修复无锁栈 push 时使用的就是 weak 版本。

    • 使用原则:如果你的算法本身就需要一个循环,那么使用 weak 可能会带来性能优势。如果你的算法逻辑不需要循环,并且你希望一次就知道结果,那么应该使用 strong

    • 使用案例:

      void push(const T& val) {
          Node<T>* newNode = new Node<T>(val);
      
          Node<T>* oldHead = head.load(std::memory_order_relaxed);
      
          do {
              newNode->next = oldHead;
          } while (!head.compare_exchange_weak(oldHead, newNode,
                                               std::memory_order_release,
                                               std::memory_order_relaxed));
      }
      
  2. 基本存取

    这是最基础的两个操作,用于安全地读取和写入原子变量。

    • std::atomic<T>::store(value)

      • 作用:原子地用 value 替换当前值。
      • 行为:这是一个单纯的写入操作。
    • std::atomic<T>::load()

      • 作用:原子地读取并返回当前值。
      • 行为:这是一个单纯的读取操作。
  3. 原子交换

    这是一个基础的“读-改-写”(Read-Modify-Write, RMW)操作。

    • std::atomic<T>::exchange(new_value)
      • 作用:原子地用 new_value 替换当前值,并返回被替换前的旧值
  4. 其他功能函数

    用于对原子变量进行一些检查相关的操作

    • is_lock_free()

      • 作用:返回 true 说明该原子类型操作是无锁的,用的是原子指令,返回 false 则是用锁
    • is_trivially_copyable()

      • 作用: 原子类型是自定义类型,该自定义类型必须可平凡复制(trivially copyable),也就意味着该类型不能有虚函数或虚基类。用来检测是否满足条件。
      • 案例:
        class A {
         public:
          virtual void f() {}
        };
        
        assert(!std::is_trivially_copyable_v<A>);
        std::atomic<A> a;                 // 错误:A 不满足 trivially copyable
        std::atomic<std::vector<int>> v;  // 错误
        std::atomic<std::string> s;       // 错误
        
  5. 最基础的原语:std::atomic_flag

  • 唯一被标准保证在所有平台上都无锁的类型。
  • 接口极简,主要是 test_and_set()(测试旧值并设置为true)和 clear()(设置为false)。
  • 它是构建其他同步原语(如自旋锁)的理论基石。

三、泛型编程约束

3.1. 可平凡复制

  • 规则: std::atomic<T> 的模板参数 T 必须是可平凡复制的。

  • 原因: CPU 原子指令操作的是纯粹的内存位,不理解 C++ 的构造/析构函数、虚函数表等复杂概念。可平凡复制的类型保证了其内存布局是“朴实”的,可以被硬件安全地按位操作。

  • 检测: 可在编译时通过 <type_traits> 中的 std::is_trivially_copyable_v<T> 来检查。

  • 失败案例:

    class A { virtual void f() {} };
    // 以下都会导致编译错误
    // std::atomic<A> a;                 
    // std::atomic<std::vector<int>> v;  
    // std::atomic<std::string> s;      
    

3.2 自由函数

1. 自由函数 (atomic_xxx)

  • 对于每个成员函数 a.func(),几乎都有一个等价的自由函数 atomic_func(&a)
  • 主要目的是为了与 C11 的 <stdatomic.h> 兼容,因为 C 语言没有成员函数,只能使用指针传递。

2. _explicit 后缀

  • 默认的自由函数使用最强的内存顺序 seq_cst

  • 带有 _explicit 后缀的版本(如 atomic_load_explicit)接受一个额外的 memory_order 参数,用于性能调优。

3.3 智能指针操作

1. C++20 之前的状况

  • std::atomic<T> 不支持 std::shared_ptr<T>
  • <memory> 头文件提供了一套特殊的自由函数std::atomic_load, std::atomic_store 等)来对非原子std::shared_ptr 变量进行原子操作,作为一种“特例”存在。
  •   std::shared_ptr<int> global_ptr;
    
      void update_ptr() {
          auto new_ptr = std::make_shared<int>(42);
          std::atomic_store(&global_ptr, new_ptr);
      }
    
      void read_ptr() {
          auto local = std::atomic_load(&global_ptr);
          // 使用 local 读取值
      }
    
    
    这些函数是重载,不是模板泛化,因此不会触发 trivially_copyable 限制。

2. C++20

  • 正式引入了 std::atomic<std::shared_ptr<T>> 的完整类模板特化。
  • 接口统一:现在可以像其他原子类型一样,直接定义 std::atomic<std::shared_ptr<T>> p; 并使用 p.load(), p.store() 等成员函数。
  • 保证无锁:标准强制要求其实现必须是无锁的。
  • std::atomic<std::shared_ptr<int>> p;
    
    p.store(std::make_shared<int>(123));  // 原子存储
    auto v = p.load();                    // 原子读取