无锁数据结构二-乱序控制(栅栏)

内存栅栏

由于优化会导致对代码的乱序执行,在并发执行时可能带来问题。因此为了并行代码的正确执行,我们需提示处理器对代码优化做一些限制。而这些提示就是内存栅栏(memory barriers),用来对内存访问进行管理。要详细了解内存栅栏原理及产生原因,可参考无锁数据结构(基础篇):内存栅障。每种处理器架构都能提供一组完整的内存栅栏供开发使用,使用这些,我们能建立不同的内存模型。通过内存模型,我们能控制并发的执行顺序,也即同步。

下面先对乱序进行一些介绍,可参考3,4

乱序

乱序分为编译器乱序和处理器(cpu)乱序,下面是我对它们的一些了解。

编译器乱序

  1. 静态分析乱序;
  2. 可从全局视图进行乱序(跨过程);

处理器乱序

  1. 处理器可以获知程序的运行时动态行为,可以动态对指令进行乱序执行。
  2. 乱序范围小,只有有限分析范围。

由于乱序可在不同层进行,栅栏也有多种,可分为编译器内存栅栏(编译器),内存栅栏(cpu)。对于c++11 来说其定义内存模型,std::memory_order对编译器和cpu都会施加影响。下面主要介绍c++11的内存模型。

内存模型与执行顺序

有如下三种内存模型:

  1. 序列一致性模型
  2. 释放/获取语义模型
  3. 宽松的内存序列化模型

可分成四种执行顺序

  1. 序列一致顺序(Sequentially-consistent ordering)
  2. 释放获取顺序(Release-Acquire ordering)
  3. 释放消费顺序(Release-Consume ordering)
  4. 宽松顺序(Relaxed ordering)

所有这些内存模型定义在一个C++列表中– std::memory_order,包含以下六个常量:

  • memory_order_seq_cst 指向序列一致性模型
  • memory_order_acquire, memory_order_release,memory_order_acq_rel, memory_order_consume 指向基于获取/释放语义的模型
  • memory_order_relaxed 指向宽松的内存序列化模型

其中:

  • memory_order_relaxed 只保证此操作是原子的,不保证任何读写内存顺序。
  • memory_order_consume 对于当前线程依赖于当前原子变量A的变量的读或写不能重排到此位置前,其他线程对依赖于A的变量的在释放A前(如store(memory_order_release))的读写,对于当前线程都是可见的(释放/消费语义)。在大多数平台上,这只影响到编译器优化> 此语义作为一个“内存的礼物”被引入DECAlpha处理器中。
  • memory_order_acquire 带此内存顺序的加载操作,在其影响的内存位置进行获得操作:当前线程中读或写不能能被重排到此加载前。其他释放同一原子变量的线程的所有写入,为当前线程所可见(释放/获取语义)。与memory_order_consume区别:此标志影响所有变量读写,而consume影响依赖原子变量的读写
  • memory_order_release 带此内存顺序的存储操作进行释放操作:当前进程中的读或写不能被重排到此存储后。当前线程的所有写入,可见于获得(acquire)该同一原子变量的其他线程(释放/获取语义),并且对该原子变量的带依赖写入变得对于其他消费同一原子对象的线程可见(释放/消费语义)。
  • memory_order_acq_rel 带此内存顺序的读-修改-写操作既是获得操作又是释放操作。当前线程的读或写内存不能被重排到此存储前或后。所有释放同一原子变量的线程的写入可见于修改之前,而且修改可见于其他获得同一原子变量的线程。
  • memory_order_seq_cst 任何带此内存顺序的操作既是获得操作又是释放操作,加上存在一个单独全序,其中所有线程以同一顺序观测到所有修改(序列一致)。

针对读(加载),可选memory_order_acquire和 memory_order_consume。针对写(存储),仅能选memory_order_release。Memory_order_acq_rel是唯一可以用来做RMW运算,比如compare_exchange, exchange, fetch_xxx。事实上,因为RMW可以并发执行原子读\写,原子性RMW原语拥有获取语义memory_order_acquire, 释放语义memory_order_release 或者 memory_order_acq_rel.
-memory_order_acq_rel – is somehow similar to memory_order_seq_cst, but RMW-operation is located inside the acquire/release-section -memory_order_relaxed – RMW-operation shifting (its load and store parts) upwards/downwards the code (for example, within the acquire/release section, if the operation is located inside such section) doesn’t lead to errors

c++11可通过下列语句组合实现内存模型

std::atomic::load
syd::atomic::store
...
...
std::atomic_thread_fence


一般情况下上图左右(store(memory_order_release)atomic_thread_fence(memory_order_release))可以互相替换,但其仍有区别,Release Fence更加严格,其阻止下方的store穿过它,而Release Operation不行,如下,m_instance的store可在g_dummy.store上:

Singleton* tmp = new Singleton;
g_dummy.store(0, std::memory_order_release);
m_instance.store(tmp, std::memory_order_relaxed);

为简单,在介绍模型时只介绍load/store

下面是我对各模型的理解

序列一致顺序

这是一种严格的内存模型,它确保处理器按程序本身既定顺序执行。

带标签 memory_order_seq_cst 的原子操作不仅以与释放/获得顺序相同的方式排序内存(在一个线程中发生先于存储的任何结果都变成做加载的线程中的可见副效应),还对所有拥有此标签的内存操作建立一个单独全序。

释放获取顺序

同步仅建立在释放和获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序。
在强顺序系统( x86 、 SPARC TSO 、 IBM 主框架)上,释放获得顺序对于多数操作是自动进行的。无需为此同步模式添加额外的 CPU 指令,只有某些编译器优化受影响(例如,编译器被禁止将非原子存储移到原子存储-释放后,或将非原子加载移到原子加载-获得前)。在弱顺序系统( ARM 、 Itanium 、 Power PC )上,必须使用特别的 CPU 加载或内存栅栏指令。

通过下面代码来说明此模型:

#include <thread>
#include <atomic>
#include <cassert>
#include <vector>

std::vector<int> data;
std::atomic<int> flag = {0};

void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);//memory_order_release 保证data.push_back(42)执行顺序一定在store前
}

void thread_2()
{
    int expected=1;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) { //memory_order_acq_rel具有release和acquire,int expected=1;在前,expected = 1;在后
    //只有当thread_1的store执行后才会进入循环,此时能保证data==42,同时memory_order_acq_rel
        expected = 1; 
    }
}

void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;//只有在执行了flag.compare_exchange_strong后,才跳出循环,此时已保证data==42
    assert(data.at(0) == 42); // 决不出错
}

int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}

释放消费顺序

使用memory_order_consume替换memory_order_acquire来提供比memory_order_acquire更弱的控制。

同步仅在释放和消费同一原子对象的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。

所有异于 DEC Alphi 的主流 CPU 上,依赖顺序是自动的,无需为此同步模式产生附加的 CPU 指令,只有某些编译器优化收益受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。

注意当前(2015年2月)没有产品编译器跟踪依赖链:消费操作被提升成获得操作。

释放消费顺序的规范正在修订中,而且暂时不鼓励使用 memory_order_consume (C++17 起)

例子如下:

#include <thread>
#include <atomic>
#include <cassert>
#include <string>

std::atomic<std::string*> ptr;
int data;

void producer()
{
    std::string* p  = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}

void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume)))
        ;
    assert(*p2 == "Hello"); // 绝无出错: *p2 从 ptr 携带依赖
    assert(data == 42); // 可能也可能不会出错: data 不从 ptr 携带依赖,可能在ptr.load前执行。
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

此顺序的典型使用情况,涉及对很少被写入的数据结构(安排表、配置、安全策略、防火墙规则等)的共时读取,和有指针中介发布的发布者-订阅者情形,即当生产者发布消费者能通过其访问信息的指针之时:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的一个例子是 rcu 解引用.

宽松顺序

提供最弱控制,只保证原子性

典型使用是计数器自增,例如std::shared_ptr 的引用计数器,因为这只要求原子性,但不要求顺序或同步

参考文档

  1. std::memory_order
  2. 无锁数据结构(基础篇):内存模型[en]
  3. 当目标CPU具有乱序执行的能力时,编译器做指令重排序优化的意义有多大?
  4. 内存栅障
  5. Acquire and Release Fences Don’t Work the Way You’d Expect
  6. 关于Singleton中使用DCLP和Memory barrier的一点疑问?
  7. Memory Barriers Are Like Source Control Operations
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

紫无之紫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值