• C++Atomic与内存序


    这篇文章简述C++11之后的内存模型和Atomic中使用的内存序(memory order)。个人作品,禁止转载

    参考文献

    Memory Models for C/C++ Programmers

    相关概念

    happens-before

    如果语句A和语句B在代码中依次出现,且B的运行依赖A产生的结果,则A必须在B之前执行且B能够看到A的结果,即A happens before B

    // example
    auto A = 3;
    auto B = A * A + 123;
    auto C = 10;
    
    • 1
    • 2
    • 3
    • 4

    则程序的正确运行必须保证A happens before B,但是不需要保证A/B happens before C。

    store & load

    store即向内存写入,load即从内存读取

    synchronized-with

    举例说明:考虑2个线程

    // thread A
    prepare_data();
    ready_flag = true;	// <---------------------- point A1
    do_other_things();
    
    • 1
    • 2
    • 3
    • 4
    // thread B
    while(ready_flag == false);	// <----------------------- point B1
    process_data();
    
    • 1
    • 2
    • 3

    A线程按顺序执行完point A1语句之后,B线程开始执行point B1之后的语句。这种关系是一种同步关系,即B synchronized-with A。这只是一个例子,并非synchronized-with的准确定义

    内存模型与内存序

    重排

    再看这个例子

    // thread A
    prepare_data();
    ready_flag = true;	// <---------------------- point A1
    do_other_things();
    
    • 1
    • 2
    • 3
    • 4

    现代计算机的CPU比内存访问快太多,因此出于性能考虑,要将部分指令重排,以减少内存访问次数。比如编译器或者CPU可能将如上片段重排为

    ready_flag = true;	// <---------------------- point A1
    prepare_data();
    do_other_things();
    
    • 1
    • 2
    • 3

    这使得程序执行错误。因为编译器并不知道ready_flag = true;这句话不能向上重排。因此需要对指令重排进行限制,具体体现为对内存序。

    Atomic

    C++的原子类型,定义了如下方法

    load(std::memory_order order) 
    store(T desired, std::memory_order order)
    exchange(T desired, std::memory_order order)
    compare_exchange_weak(T& expected, T desired, std::memory_order order)
    compare_exchange_strong(T& expected, T desired, std::memory_order order)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    对整数类型和浮点类型,定义了自加和自减

    fetch_add(T arg, std::memory_order order)
    fetch_sub(T arg, std::memory_order order)
    
    • 1
    • 2

    对整数类型,还定义了原子位运算

    fetch_and(T arg, std::memory_order order)
    fetch_or(T arg, std::memory_order order)
    fetch_xor(T arg, std::memory_order order)
    
    • 1
    • 2
    • 3

    默认情况下,memory_order 为 memory_order_seq_cst,即顺序一致性。比如

    atomic i32;
    i32++;	// memory_order_seq_cst
    i32.fetch_add(2); // memory_order_seq_cst
    
    • 1
    • 2
    • 3

    内存序

    C++11定义了如下内存序

    memory_order_seq_cst
    memory_order_acq_rel
    memory_order_release
    memory_order_acquire
    memory_order_consume
    memory_order_relaxed
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其中

    • memory_order_acquire,memory_order_consume只能用于读操作
    • memory_order_release只能用于写操作
    • memory_order_acq_rel只能用于读-改-写操作
    • memory_order_relaxed即不需要限制内存序,只需要保证原子性
    • memory_order_seq_cst顺序一致性,即不允许任何重排,默认
      解释如下(如下解释中,load和store都是针对同一个atomic变量)
    内存序解释
    memory_order_acquire当前load操作之后的所有读、写操作都不能被重排到该load操作上方
    memory_order_comsume当前load操作之后的,所有依赖于当前所load的变量的读、写操作都不能被重排到该load操作上方
    memory_order_acq_rel本线程中,所有该操作上方的读写操作都不能被重排到该操作下方,该操作下方的读写操作也不能被重排到该操作上方。其它线程中,用release进行的store操作之前的所有写操作,都在该读写改操作之前可见
    memory_order_release本线程中该操作之前的所有读写操作都不能被重排到该操作下方
    memory_order_seq_cst可以用于store和load,也可以用于读写改。该操作之前的所有读写,不允许被重排到该操作下方;该操作之后的所有读写,不允许被重排到该操作上方

    原子量线程同步的例子

    如下三种内存序组合,可以用于线程同步

    • memory_order_seq_cst and memory_order_seq_cst
    • memory_order_release and memory_order_acquire
    • memory_order_release and memory_order_consume

    memory_order_seq_cst and memory_order_seq_cst

    // thread A
    c = 0;
    x = 0;
    a = 1;
    b = another_variable;
    x.store(1, std::memory_order_seq_cst);     // <----------------Point A
    c = 2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    // thread B
    cout << c << endl;
    while(x.load(std::memory_order_seq_cst) == 1);	// <----------------Point A
    cout << a << endl;
    
    • 1
    • 2
    • 3
    • 4

    线程B的输出,只有可能是0,1。在Point A之前,A线程中a必然已经被赋值,c必然是0。Point A之前的读写操作不会跑到Point A之后,反之亦然

    memory_order_release and memory_order_acquire

    // thread A
    c = 0;
    ......
    b = 1;
    x.store(1, std::memory_order_release);     // <----------------Point A
    c = 2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // thread B
    while(x.load(std::memory_order_acquire) == 1);    // <----------------Point A
    cout << b << c << endl;
    
    • 1
    • 2
    • 3

    线程B的输出,变量b一定是1,因为x.store(1, std::memory_order_release); 之前的读写操作都不能被重排到它下方,线程B的load操作之后的所有读写操作,都不会被重排到它上方。c可能是0或者2,因为memory_order_release并不限制store操作之后的读写操作的重排。

    memory_order_release and memory_order_consume

    // thread A
    c = 0; b = 0;
    ......
    b = 1;
    x.store(1, std::memory_order_release);     // <----------------Point A
    c = 2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    // thread B
    while(x.load(std::memory_order_consume) == 1);    // <----------------Point A
    cout << b << c << endl;
    cout << x << endl;
    
    • 1
    • 2
    • 3
    • 4

    线程B的输出,b可能是1和0,因为b可能在x.load之前就读取了;c可能是0或者2,因为它并没有被原子量限制读取顺序,x必然是1,因为x的值和x.load有关,它不能被重排到x.load上方

    实践中如何使用

    • 如果没搞清楚,就用默认的memory_order_seq_cst
    • 如果只需要原子性,不需要线程间同步,就用memory_order_relaxed。比如需要多线程共享一个计数器,但是并不需要该计数器绝对准确。

    相关概念:内存屏障

    之后的C++版本还引入了内存屏障,和内存序相似

    • acquire_memory_fence( void )ensures that all subsequent operations in program order are performed after all preceding loads in program order;
    • release_memory_fence( void )ensures that all preceding operations in program order are performed before all subsequent stores in program order;
    • acq_rel_memory_fence( void ), combines the semantics of acquire and release;
    • ordered_memory_fence( void ), ensures that all preceding operations in program order are performed before all subsequent operations in program order.
  • 相关阅读:
    linux驱动-CCF-2 of_clk_provider
    混沌工程-经典案例分享
    北京十大律师事务所排名前十名(8月最新发布)
    TiDB Lightning 配置参数
    如何评估企业的数据质量
    List详解
    Antv/G2 自定义tooltip鼠标悬浮提示信息
    自动化驱动程序管理
    .NET AppDomain、Process 、Context
    非零基础自学Java (老师:韩顺平) 第8章 面向对象编程(中级部分) 8.9 super关键字
  • 原文地址:https://blog.csdn.net/WilliamCode/article/details/126139289