zoukankan      html  css  js  c++  java
  • 理解 C++ 的 Memory Order

    为什么需要 Memory Order

      如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果:

    • 即使是简单的语句,C++ 也不保证是原子操作。
    • CPU 可能会调整指令的执行顺序。
    • 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。

      原子操作说的是,一个操作的状态要么就是未执行,要么就是已完成,不会看见中间状态。例如,在 C++11 中,下面程序的结果是未定义的:

    1
    2
    3
    4
    int64_t i = 0; // global variable
     
    Thread-1: Thread-2:
    i = 100; std::cout << i;

      C++ 并不保证i = 100是原子操作,因为在某些 CPU Architecture 中,写入int64_t需要两个 CPU 指令,所以 Thread-2 可能会读取到i在赋值过程的中间状态。


      另一方面,为了优化程序的执行性能,CPU 可能会调整指令的执行顺序。为阐述这一点,下面的例子中,让我们假设所有操作都是原子操作:

    1
    2
    3
    4
    5
    6
    7
    int x = 0; // global variable
    int y = 0; // global variable
     
    Thread-1: Thread-2:
    x = 100; while (y != 200)
    y = 200; ;
    std::cout << x;

      如果 CPU 没有乱序执行指令,那么 Thread-2 将输出100。然而,对于 Thread-1 来说,x = 100;y = 200;这两个语句之间没有依赖关系,因此,Thread-1 允许调整语句的执行顺序:

    1
    2
    3
    Thread-1:
    y = 200;
    x = 100;

      在这种情况下,Thread-2 将输出0100


      CPU cache 也会影响到程序的行为。下面的例子中,假设从时间上来讲,A 操作先于 B 操作发生:

    1
    2
    3
    4
    int x = 0; // global variable
     
    Thread-1: Thread-2:
    x = 100; // A std::cout << x; // B

      尽管从时间上来讲,A 先于 B,但 CPU cache 的影响下,Thread-2 不能保证立即看到 A 操作的结果,所以 Thread-2 可能输出0100


      从上面的三个例子可以看到,多线程读写同一变量需要使用同步机制,最常见的同步机制就是std::mutexstd::atomic。然而,从性能角度看,通常使用std::atomic会获得更好的性能。
      C++11 为std::atomic提供了 4 种 memory ordering:

    • Relaxed ordering
    • Release-Acquire ordering
    • Release-Consume ordering
    • Sequentially-consistent ordering

      默认情况下,std::atomic使用的是 Sequentially-consistent ordering。但在某些场景下,合理使用其它三种 ordering,可以让编译器优化生成的代码,从而提高性能。

    Relaxed ordering

      在这种模型下,std::atomicload()store()都要带上memory_order_relaxed参数。Relaxed ordering 仅仅保证load()store()是原子操作,除此之外,不提供任何跨线程的同步。
      先看看一个简单的例子:

    1
    2
    3
    4
    5
    6
    std::atomic<int> x = 0; // global variable
    std::atomic<int> y = 0; // global variable
     
    Thread-1: Thread-2:
    r1 = y.load(memory_order_relaxed); // A r2 = x.load(memory_order_relaxed); // C
    x.store(r1, memory_order_relaxed); // B y.store(42, memory_order_relaxed); // D

      执行完上面的程序,可能出现r1 == r2 == 42。理解这一点并不难,因为编译器允许调整 C 和 D 的执行顺序。如果程序的执行顺序是 D -> A -> B -> C,那么就会出现r1 == r2 == 42


      如果某个操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering。程序计数器是一种典型的应用场景:

    root@ubuntu:/data1# cat test7.cpp 
    #include <cassert>
    #include <vector>
    #include <iostream>
    #include <thread>
    #include <atomic>
    std::atomic<int> cnt = {0};
    void f()
    {
        for (int n = 0; n < 1000; ++n) {
            cnt.fetch_add(1, std::memory_order_relaxed);
        }
    }
    int main()
    {
        std::vector<std::thread> v;
        for (int n = 0; n < 10; ++n) {
            v.emplace_back(f);
        }
        for (auto& t : v) {
            t.join();
        }
        assert(cnt == 10000);    // never failed
        return 0;
    }
    root@ubuntu:/data1#  g++ -g -Wall -std=c++11  -pthread test7.cpp -o  test7
    root@ubuntu:/data1# ./test7
    root@ubuntu:/data1# 

    Release-Acquire ordering

      在这种模型下,store()使用memory_order_release,而load()使用memory_order_acquire。这种模型有两种效果,第一种是可以限制 CPU 指令的重排:

    • store()之前的所有读写操作,不允许被移动到这个store()的后面。
    • load()之后的所有读写操作,不允许被移动到这个load()的前面。

      除此之外,还有另一种效果:假设 Thread-1 store()的那个值,成功被 Thread-2 load()到了,那么 Thread-1 在store()之前对内存的所有写入操作,此时对 Thread-2 来说,都是可见的。
      下面的例子阐述了这种模型的原理:

    #include <thread>
    #include <atomic>
    #include <cassert>
    #include <string>
    std::atomic<bool> ready{ false };
    int data = 0;
    void producer()
    {
        data = 100;                                       // A
        ready.store(true, std::memory_order_release);     // B
    }
    void consumer()
    {
        while (!ready.load(std::memory_order_acquire))    // C
            ;
        assert(data == 100); // never failed              // D
    }
    int main()
    {
        std::thread t1(producer);
        std::thread t2(consumer);
        t1.join();
        t2.join();
        return 0;
    }
    root@ubuntu:/data1#  g++ -g -Wall -std=c++11  -pthread test8.cpp -o  test8
    root@ubuntu:/data1# ./test8 

    让我们分析一下这个过程:

    • 首先 A 不允许被移动到 B 的后面。
    • 同样 D 也不允许被移动到 C 的前面。
    • 当 C 从 while 循环中退出了,说明 C 读取到了 B store()的那个值,此时,Thread-2 保证能够看见 Thread-1 执行 B 之前的所有写入操作(也即是 A)。

    http://senlinzhan.github.io/2017/12/04/cpp-memory-order/

    http://senlinzhan.github.io/2017/02/26/cpp11_thread/

  • 相关阅读:
    欧拉函数模板
    Django Views Decorator
    Anaconda3 安装报错 bunzip2: command not found
    Windows 错误 0x80070570
    GitHub报错error: bad signature
    failed to push some refs to 'git@github.com:RocsSun/mytest.git
    更新GitHub的仓库
    Git连接GitHub
    Git的初始化设置
    Git的选项参数
  • 原文地址:https://www.cnblogs.com/dream397/p/14541845.html
Copyright © 2011-2022 走看看