美文网首页C++2.0程序员C++
C++11 内存模型(简明版)

C++11 内存模型(简明版)

作者: 找不到工作 | 来源:发表于2017-08-16 22:49 被阅读128次

    参考

    英文资料1
    英文资料2
    中文资料1
    中文资料2

    为什么要写这篇文章

    项目需求,需要实现 lock-free 的并行写文件。
    深入理解内存模型是实现高性能并行程序的基本,因此需要将 C++11 的 atomic 相关内容细细研读。
    网上看了不少相关资料,大部分给我的感觉是,偏深偏难不实用。不适合我这种菜鸡看。于是只好自己动手写一篇。

    为什么需要内存模型

    先进一段代码

    #include <thread>
    #include <vector>
    
    int main(int argc, char const *argv[]) {
      int a = 1;
      int b = 100;
      std::vector<std::thread> threads;
      threads.emplace_back([&](){a = 2; printf("b = %d\n", b);});
      threads.emplace_back([&](){b = 200; printf("a = %d\n", a);});
      for (auto& t : threads) {
        t.join();
      }
      return 0;
    }
    

    猜猜这段代码的输出是什么?有可能存在 a = 1, b = 100 的输出吗?

    乱序执行

    首先需要推翻的一个观点是,单个 CPU 只能串行执行指令。
    现代cpu都采用流水线结构,流水线的各级可以同时执行不同的指令,也只有用多条指令将流水线填满以后,cpu的能力才能得到充分发挥。
    编译器和 CPU 都会对你的代码进行优化,为了实现更好的性能。例如,有如下操作:

    B = func(3)  // 1
    A = B+1  // 2 
    C = 7  // 3
    

    注意到,语句 2 依赖于语句 1 的结果,而语句 3 是独立的。
    假设目前有 1 个 CPU 正在处理这段代码,那么它在等待获取 B 的值的时候其实空闲流水线可以先处理语句 3 的代码。这就是为什么需要乱序优化。
    乱序优化也是有原则的,那就是保证在单核情况下运行的效果是不变的

    乱序导致的问题

    但是,现在已经进入了多核时代,于是乱序就会导致问题。例如:
    伪代码如下所示,能否预测 Q 和 D 最终结果是多少?

    A := 1
    B := 2
    C := 3
    P := &A
    Q := &C
    
    // CPU 1
    B = 4 
    P = &B 
    
    // CPU 2
    Q = P 
    D = *Q 
    

    这个例子中,CPU2 要执行的指令有明显的依赖关系,所以顺序不会改变。因为 D=*Q 依赖于 Q 指针。所以需要先执行 Q=P
    CPU1 要执行的指令看起来似乎也有依赖关系,但实际是没有。因为改变 B 的值不会改变 B 的地址。也就是说,倒序执行,最终 B 和 P 中的值是一样的。所以 CPU1 在这里不一定会按照顺序执行。
    可能出现以下情况,第三种情况比较特殊。

    1. CPU1 还未执行 P=&B,CPU 2 执行结束
      此时应该有 Q = &A, D = 1
    2. CPU1 执行了 P=&B,CPU2 执行结束
      这时候必定已经执行了B=4,因此有 Q = &B, D = 4
    3. CPU1 乱序执行,先执行了 P=&B,CPU2 执行结束,但是还未执行 B=4
      此时会有 Q=&B, D = 2

    如何解决

    如上面例子所述,在多核系统上,如果不施加任何限制,当多个线程同时读写共享的变量的时候,一个线程可能观察到值的变化于另一个线程写的顺序不同。
    要解决这个问题,我们需要定义内存的访问顺序。

    四种常用内存模型

    我们将重点介绍 release-acquire 模型,它是实现 lock-free 编程的重点。

    std::memory_order_relaxed

    最宽松的内存模型,效率也最高。实际上它不属于同步操作,因为它不对内存访问做出任何顺序限制。仅仅保证操作的原子性。

    一般用于多线程计数器。例如 std::shared_ptr 的引用计数就是利用这个实现的。

    std::memory_order_release 和 std::memory_order_acquire

    这两者是需要搭配使用的。构成一种 release-acquire 模型。
    std::memory_order_acquire
    这是读操作 (load) 时可以指定的内存顺序。对作用的内存区域产生效果:

    1. 在这次 load 之前当前线程的读写不允许乱序。
    2. release 同一原子量的线程中的写操作在当前线程可见。

    std::memory_order_release
    这是写操作 (store) 时可以指定的内存顺序。对作用的内存区域产生如下效果:

    1. 在这次 store 之后当前线程的读写不允许乱序。
    2. acquire 同一原子量的线程可以看到当前线程的所有写操作。

    用人话来讲就是,在两个线程中建立了同步关系(synchronize-with),在 release 之前发生的所有事,在 acquire 之后都是可见的。

    release-acquire

    下面是一个利用原子操作来解决 Double-Checked Locking 线程不安全问题的例子。

    std::atomic<Singleton*> Singleton::m_instance;
    std::mutex Singleton::m_mutex;
    
    Singleton* Singleton::getInstance() {
        Singleton* tmp = m_instance.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::lock_guard<std::mutex> lock(m_mutex);
            tmp = m_instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
                tmp = new Singleton;
                m_instance.store(tmp, std::memory_order_release);
            }
        }
        return tmp;
    }
    

    如果不增加这个原子操作,会出现以下问题:

    1. A 线程调用,发现还未构造,于是获取锁开始进行构造。
    2. tmp = new Singleton 其实是分为两步,第一步分配内存,第二步构造对象。分配内存后 tmp 指针就已经不是空了。但是还没执行构造函数。
    3. B 线程刚好此时插入,检查 tmp 非空,于是直接返回了一个没有构造完成的对象。

    因此必须要使用原子操作来同步。在新建实例成功后再 release,则 acquire 的操作时必然可见的是一个构造好了的对象。

    mutex 和 spinlock 都是它的典型应用。

    典型的 spinlock 实现:

    std::atomic_flag spinlock = ATOMIC_FLAG_INIT;
    // lock
    while (spinlock.test_and_set(std::memory_order_acquire)) {
    }
    // critical area
    // unlock
    spinlock.clear(std::memory_order_release);
    

    由于在上锁之前, acquire 特性保证了不可乱序,而解锁之后,release 特性又保证了不可乱序,中间则只能有一个线程执行,因为只有一个线程能获得锁,因此乱序也无妨。所以一定是安全的。

    std::memory_order_release 和 std:: memory_order_consume

    不建议使用。

    std::memory_order_seq_cst

    顺序一致模型(sequence-consistent)。任何操作都同时是 acquire 操作和 release 操作。在所有线程上都观察到改变是同一顺序的(与作出修改的线程一致)。这是默认的模型,如果不为原子操作指定参数,则就采用这个模型。性能最差,但是符合逻辑。

    相关文章

      网友评论

        本文标题:C++11 内存模型(简明版)

        本文链接:https://www.haomeiwen.com/subject/ztdmrxtx.html