C++ `std::atomic_store` / `std::atomic_load` 与内存顺序的精确控制

哈喽,各位好!今天我们要聊的是C++中原子操作的两位重量级选手:std::atomic_storestd::atomic_load,以及它们背后的内存顺序控制。这玩意儿听起来玄乎,但其实没那么难。想象一下,多线程就像一群熊孩子在厨房里做饭,如果没有规则,那场面……简直是灾难!原子操作和内存顺序就是用来约束这些熊孩子的行为,确保他们能安全、正确地完成任务。

什么是原子操作?

首先,我们要搞清楚什么是原子操作。原子操作就像一个“要么全做,要么全不做”的事务。举个例子,你银行卡里有100块钱,想转给朋友50块。这个转账操作,必须是账户先扣50,然后朋友账户加50,这两个步骤要打包成一个原子操作。如果只扣了你的钱,朋友没收到,那你就亏大了,银行也得倒闭。

在多线程环境下,原子操作保证了对共享变量的操作不会被其他线程中断。也就是说,当一个线程正在修改一个原子变量时,其他线程要么看到修改前的状态,要么看到修改后的状态,绝对不会看到中间状态。

std::atomic_storestd::atomic_load:闪亮登场

std::atomic_storestd::atomic_load 是 C++11 引入的原子操作函数,分别用于原子地存储和加载原子变量的值。

  • std::atomic_store(atomic_object, value): 将 value 原子地存储到 atomic_object 中。
  • std::atomic_load(atomic_object): 原子地从 atomic_object 中加载值。

简单来说,store 是写操作,load 是读操作。

代码示例:

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> counter(0);

void increment() {
  for (int i = 0; i < 100000; ++i) {
    counter++; // 等价于 counter.fetch_add(1, std::memory_order_relaxed);
  }
}

int main() {
  std::thread t1(increment);
  std::thread t2(increment);

  t1.join();
  t2.join();

  std::cout << "Counter value: " << counter << std::endl; // 预期输出:200000
  return 0;
}

在这个例子中,counter 是一个原子变量,increment 函数通过 counter++ (实际上是 fetch_add 的简写) 来增加它的值。由于 counter 是原子的,所以即使多个线程同时增加它,最终的结果也是正确的。如果 counter 不是原子的,那么由于数据竞争,最终的结果可能小于 200000。

内存顺序:熊孩子的行为准则

现在,我们来聊聊内存顺序。内存顺序定义了编译器和 CPU 可以对内存操作进行重排序的约束。不同的内存顺序提供了不同的性能和同步保证。想象一下,如果熊孩子做饭的步骤可以随意颠倒(比如先吃菜再洗菜),那结果肯定惨不忍睹。内存顺序就是用来规定这些步骤的先后顺序。

C++ 提供了六种内存顺序:

内存顺序 描述 性能 同步成本
std::memory_order_relaxed 最宽松的顺序。只保证操作的原子性,不提供任何同步保证。编译器和 CPU 可以随意重排序操作。 最高 最低
std::memory_order_consume 用于依赖链的同步。如果一个线程读取了另一个线程使用 std::memory_order_release 写入的值,并且依赖于该值进行后续操作,那么 std::memory_order_consume 可以保证这些后续操作不会被重排序到读取操作之前。 注意:这个很少用,基本可以忽略 较高 较低
std::memory_order_acquire 用于同步。保证在读取操作之后的所有读写操作都不会被重排序到读取操作之前。通常与 std::memory_order_release 配合使用,实现线程间的同步。 中等 中等
std::memory_order_release 用于同步。保证在写入操作之前的所有读写操作都不会被重排序到写入操作之后。通常与 std::memory_order_acquire 配合使用,实现线程间的同步。 中等 中等
std::memory_order_acq_rel 结合了 std::memory_order_acquirestd::memory_order_release 的特性。既保证在操作之后的所有读写操作都不会被重排序到操作之前,又保证在操作之前的所有读写操作都不会被重排序到操作之后。通常用于修改-读取-修改 (RMW) 操作。 较低 较高
std::memory_order_seq_cst 最严格的顺序。保证所有线程看到的操作顺序都是一致的。这是默认的内存顺序。 最低 最高

内存顺序详解:

  • std::memory_order_relaxed:无拘无束的熊孩子

    这是最宽松的内存顺序,只保证原子性,不保证任何顺序。编译器和 CPU 可以随意地对读写操作进行重排序。这意味着,一个线程写入的值,其他线程可能不会立即看到。

    适用场景: 只需要保证操作的原子性,不需要保证线程间的同步。例如,计数器。

    代码示例:

    #include <iostream>
    #include <atomic>
    #include <thread>
    
    std::atomic<int> x(0);
    std::atomic<int> y(0);
    
    void thread1() {
      x.store(1, std::memory_order_relaxed);
      y.store(2, std::memory_order_relaxed);
    }
    
    void thread2() {
      std::cout << "x: " << x.load(std::memory_order_relaxed) << ", y: " << y.load(std::memory_order_relaxed) << std::endl;
    }
    
    int main() {
      std::thread t1(thread1);
      std::thread t2(thread2);
    
      t1.join();
      t2.join();
    
      return 0;
    }

    在这个例子中,thread2 打印的 xy 的值可能都是 0,即使 thread1 已经分别将它们设置为 1 和 2。这是因为 std::memory_order_relaxed 不保证线程间的同步,thread2 可能在 thread1 完成写入之前就执行了读取操作。

  • std::memory_order_acquirestd::memory_order_release:配合默契的搭档

    std::memory_order_acquirestd::memory_order_release 通常配合使用,用于实现线程间的同步。

    • std::memory_order_release:释放锁。保证在该操作之前的所有读写操作都不会被重排序到该操作之后。
    • std::memory_order_acquire:获取锁。保证在该操作之后的所有读写操作都不会被重排序到该操作之前。

    适用场景: 保护共享资源,实现互斥锁。

    代码示例:

    #include <iostream>
    #include <atomic>
    #include <thread>
    
    std::atomic<bool> ready(false);
    int data = 0;
    
    void producer() {
      data = 42;
      ready.store(true, std::memory_order_release);
    }
    
    void consumer() {
      while (!ready.load(std::memory_order_acquire)); // 等待 producer 设置 ready 为 true
      std::cout << "Data: " << data << std::endl; // 保证 data 的读取发生在 ready 的读取之后
    }
    
    int main() {
      std::thread t1(producer);
      std::thread t2(consumer);
    
      t1.join();
      t2.join();
    
      return 0;
    }

    在这个例子中,producer 线程先设置 data 的值,然后使用 std::memory_order_releaseready 设置为 trueconsumer 线程使用 std::memory_order_acquire 读取 ready 的值,并等待其变为 true。由于使用了 std::memory_order_acquirestd::memory_order_release,可以保证 consumer 线程读取到的 data 的值一定是 producer 线程设置的值(42),而不是一个未初始化的值。

  • std::memory_order_acq_rel:独当一面的全能选手

    std::memory_order_acq_rel 结合了 std::memory_order_acquirestd::memory_order_release 的特性。它既可以作为释放锁使用,也可以作为获取锁使用。

    适用场景: 修改-读取-修改 (RMW) 操作,例如 fetch_addfetch_subcompare_exchange 等。

    代码示例:

    #include <iostream>
    #include <atomic>
    #include <thread>
    
    std::atomic<int> counter(0);
    
    void increment() {
      for (int i = 0; i < 100000; ++i) {
        counter.fetch_add(1, std::memory_order_acq_rel); // 原子地增加 counter 的值
      }
    }
    
    int main() {
      std::thread t1(increment);
      std::thread t2(increment);
    
      t1.join();
      t2.join();
    
      std::cout << "Counter value: " << counter << std::endl; // 预期输出:200000
      return 0;
    }

    在这个例子中,fetch_add 使用了 std::memory_order_acq_rel,既保证了原子性,又保证了线程间的同步。

  • std::memory_order_seq_cst:秩序井然的老大哥

    这是最严格的内存顺序,也是默认的内存顺序。它保证所有线程看到的操作顺序都是一致的。这意味着,如果一个线程执行了一系列原子操作,那么所有其他线程都会按照相同的顺序看到这些操作。

    适用场景: 需要保证全局一致性的场景。

    代码示例:

    #include <iostream>
    #include <atomic>
    #include <thread>
    
    std::atomic<int> a(0);
    std::atomic<int> b(0);
    
    void thread1() {
      a.store(1, std::memory_order_seq_cst);
      b.store(2, std::memory_order_seq_cst);
    }
    
    void thread2() {
      int x = b.load(std::memory_order_seq_cst);
      int y = a.load(std::memory_order_seq_cst);
    
      std::cout << "x: " << x << ", y: " << y << std::endl;
    }
    
    int main() {
      std::thread t1(thread1);
      std::thread t2(thread2);
    
      t1.join();
      t2.join();
    
      return 0;
    }

    在这个例子中,由于使用了 std::memory_order_seq_cst,可以保证 thread2 读取到的 ab 的值要么都是 0,要么 a 是 1 且 b 是 2。不可能出现 a 是 0 且 b 是 2 的情况,因为 std::memory_order_seq_cst 保证了操作的全局一致性。

选择合适的内存顺序:

选择合适的内存顺序是一个权衡的过程。更严格的内存顺序提供更强的同步保证,但也带来更高的性能开销。通常情况下,应该选择满足需求的最宽松的内存顺序。

场景 推荐的内存顺序
简单的计数器 std::memory_order_relaxed
保护共享资源(互斥锁) std::memory_order_acquire / std::memory_order_release
修改-读取-修改 (RMW) 操作 std::memory_order_acq_rel
需要保证全局一致性 std::memory_order_seq_cst

总结:

std::atomic_storestd::atomic_load 是 C++ 中原子操作的重要组成部分。通过合理地选择内存顺序,可以有效地控制多线程程序的行为,确保数据的一致性和正确性。记住,内存顺序就像熊孩子的行为准则,选择合适的准则才能让他们安全、高效地完成任务!

希望今天的讲解能够帮助大家更好地理解 std::atomic_storestd::atomic_load 以及内存顺序。多加练习,你也能成为驾驭多线程的专家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注