内存序列-memor order

内存序

今天看内存序看的要崩溃了,太乱了,不同人的博客常常出现前后不一致的情况。这个工作只是临时起意,也不是什么意义重大的工作,因此也没有寻找更权威的资料,最主要的参考是cplusplus上的reference,如果有错误之外,真心希望您来指正一下,自己真的逐个字死磕类型的。

为何需要内存序

在实际的程序运行过程中,如果不使用任何的同步原语,那么很多变量的执行结果,乃至程序的最终行为是无法预料的。具体来说,影响主要有以下几个点:

  1. 编译器指令的重新排列:编译器出于优化的考虑,会对生成的指令代码进行重新排列,而排列的代码可能在多线程运行中引起不可预料的结果。要注意的是,在单线程"顺序执行"的情况下,这种重排并不会影响程序结果。
  2. cpu cache:例如对内存的某个变量的写操作,可能先写入了cache,而并没有立刻被其他线程看到
  3. 指令的非原子性:例如某个操作需要涉及多条指令,而我们希望这个操作是个原子操作,不能被其他线程看到操作的中间结果(因为中间结果可能无任何意义)。

为了能够准确的控制内存读写操作的可见性,保证多线程的读写情况下变量结果的正确,需要一定的内存序的语义支持。传统的方案常采用机制,以控制多线程之间的并行访问,但是它的代价相对较高。然而使用内存序也就是无锁策略,常可以获得更高的性能,这也是很多“无锁数据结构”采用的招式。

C++11针对atomic原子变量的几种内存序

如下的几种内存序遵循了从“松散”到“严格”的策略,所谓“松散”就是对内存执行序列的限制比较少,而“严格”就是限制比较多。

memory_order_relaxed

最好记。松散内存序,多线程之间不会有任何的同步和内存序的限制,也就是说编译器可以任意的指定指令的重排策略。

memory_order_consume

最难理解。consume顾名思义就是“消费”,也就是读(load)操作,它要求:当前线程所有的依赖此load的原子变量的所有读写操作决不能被重排到load操作的前面

这个里面关键的两个字是“依赖”。也就是说,假定当前读取的变量是x,

如下面的代码,2不能被重排到1的前面,

// 线程1
x依赖atm

// 线程2
std::atmoic atm = 0;
z = atm.load(memory_order_consume) // 1
x = 1; // 2
y = 2; // 3

memory_order_acquire

不算难理解。 依然是读(load)操作,它要求:当前线程所有的在load之的读写操作决不能被重排到load操作的前面

如下面的代码,2和3不能被重排到1的前面,严格遵循load之后的读写操作不能被重排到load的前面。

std::atmoic atm = 0;
z = atm.load(memory_order_acquire) // 1
x = 1; // 2
y = 2; // 3

帮助记忆:acquire相当于是申请了一个开门栅栏,在这个之后的所有读写操作不能被放到栅栏的前面。

memroy_order_release

不算难理解,与acquire合起来记忆。 这是写(write)操作,它要求:当前线程中所有的在write之的读写操作决不能被重排到write操作的后面

如下面的代码,1和2不能被重排到3的后面,严格遵守上面的语句。

std::atmoic atm = 0;
x = 1; // 1
y = 2; // 2
atm.store(z, memory_order_release) // 3

帮助记忆:release可认为是释放了一个关门栅栏,在这个栅栏前的读写操作不能被放到栅栏的后面。

memory_order_acq_rel

它对应的是read-modify-write的操作的可见性,其他线程的写操作(release)在本线程的modification之前可见,且本线程的modification操作之后,能够立刻被其他线程的acquire看见。它要求:在当前线程中,所有的在此操作之前和之后的任何读写操作不能被重排到它的后面或者前面。

帮助记忆:相当于申请了一个严格的栅栏,它前面的操作只能在前面,它后面的操作只能在后面。

memory_order_seq_cst

严格一致性,所有的线程看到的都是完全一致的顺序,这个是原子变量的默认序。

四种同步模型

Relaxed ordering

带有memory_order_relaxed标记的原子变量,不遵循任何的同步语义,可能被任意的重排,只有原子保证。

如下, x和y初始化为0,

// Thread 1:
r1 = atomic_load_explicit(y, memory_order_relaxed); // A
atomic_store_explicit(x, r1, memory_order_relaxed); // B
// Thread 2:
r2 = atomic_load_explicit(x, memory_order_relaxed); // C
atomic_store_explicit(y, 42, memory_order_relaxed); // D

这可能得出r1== r2 == 42的结果,也就是遵循D->A->B->C的排列。

Relaxed ordering的典型应用是counter,不关心多线程之间的内存序的同步策略(写入之后的读可见性),相互之间没有任何依赖,只需要增加counter即可,例如shared_ptr的reference counter就是使用的relaxed order来增加引用计数的。那你可能也会说,那减少引用计数也是使用的relaxed order的吧?要告诉你,答案是NO。

如下是一个典型的代码,在降低引用计数的时候,需要额外的delete ptr的操作,而且只有最后一个释放的线程才能执行delete ptr(引用计数为1),这就涉及到多个线程之间的内存序的同步操作:其他线程的写操作必须要在当前线程fetch之前被看到,且在当前modification后,当前的结果要被其他线程的读(acquire)看到。

假定使用的relaxed order,我们来分析一种情况:引用计数当前为2,线程A尝试减少1,线程2也尝试减少1,线程1读取之后发现是2,线程2读取之后也发现是2,

class Foo {
    // ...

    std::atomic refCount_{0};

    friend void intrusive_ptr_add_ref(Foo* ptr)
    {
        ptr->refCount_.fetch_add(1, std::memory_order_relaxed); // 松散内存序
    }

    friend void intrusive_ptr_release(Foo* ptr)
    {
        if (ptr->refCount_.fetch_sub(1, std::memory_order_acq_rel ) == 1) {  
            delete ptr;
        }
    }
};

还未写完。。。


参考资料:

  • [1] http://senlinzhan.github.io/2017/12/04/cpp-memory-order/
  • [2] https://www.zhihu.com/question/24301047
  • [3]https://en.cppreference.com/w/c/atomic/memory_order

你可能感兴趣的:(C++)