• c++11 standardized memory model 内存模型


    C++11 标准中引入了内存模型,其目的是为了解决多线程中可见性和顺序(order)。这是c++11最重要的新特征,标准忽略了平台的差异,从语义层面规定了6种内存模型来实现跨平台代码的兼容性。多线程代码因为其本身的复杂性问题,有引入死锁和race condition等一系列问题,可能造成的后果有crash和random error,非常难以debug,因为人的思维是线性的,mutiple threads的代码会交叉运行.

    1、同时因为编译器优化和CPU指令集的优化,代码的顺序可能被打乱。那么在多线程环境下。我们看到的结果和我们预期的结果可能不一样。

    2、另外多CPU独立Cache Line的同步问题,多核CPU通常有自己的一级缓存和二级缓存,访问缓存的数据很快。但是如果缓存没有同步到主存和其他核心的缓存,其他核心读取缓存就会读到过期的数据。

    一个经典的例子如下:

    bool flag =false;
    int y = 0;
    // run at thread 1
    void M1() {
        y = 42;           // 1
        flag= true;            // 2
    }
    // run at thread 2
    void M2() {
        while (!flag);   //1
    y++; //2
    }

    如果不考虑reorder,thread2 中y的值肯定是43. 实际上却又两种可能的结果43和1. 为什么会出现这种情况?那就是因为指令重排了。 thread 1中step1 和step2 的顺序可以被编译器或者CPU指令重新reorder。

    那么我们就需要武器防止这种reorder的发生。在C+11 之前都是用memory barrier,用于告诉编译器和CPU 哪些地方不能reorder 顺序,同时保证可见性顺序。这就是memory model引入的目前。规定6种不同的order来帮助我们写代码;以上的代码还有race condition的问题,大家看出来了,可能会导致undefined的行为。因为不是原子性的操作,多个线程可能同时操作同一块内存,同样memory model标准应该要杜绝race condition的情况。

    如果我们保证不reorder,就一定可以得到正确的结果吗?答案是不能,我们还有考虑多线程之间可见性和顺序性问题。这与重排是不同的概念。

    在一个core修改了一个变量,另一个core立马就能读到;或者你修改了两个变量,你要求另一个core在读到这两个变量的时候,要按照相同的顺序,比如这样的代码:

    core 1x = 1024; flag = true;
    
    core 2: 
    while (!flag) ; assert(x == 1024);
    

    这段代码其实就假定了几件事情,对变量x的修改,要先于对flag的修改;并且在core 2中要感知到这样的顺序。

    综上,在多线程中,要保证线程之间的同步问题,需要规定编译器和CPU优化,和CPU cache生效等两大方面问题。C++11 memory model完美解决这两类问题,逻辑层面的东西只能通过语言层面引入model来解决,硬件本身解决不了,编译器无法预知你编码逻辑顺序。
    自己期望的代码顺序需要自己保证。工具就是memory model
     

    可能有同学会说,有这么复杂吗?我们平常不是直接加mutex互斥锁来保证多线程代码的正确性吗?是的,加锁mutex是通用的办法,加mutex互斥锁的目的是为了多线程之间的顺序性。线程和线程之间通过争取mutex锁,来实现代码正确的order执行顺序。mutex临界区保护的代码区不会跳出临界区的约束。只能等到获取mutex之后才能执行。mutex锁区域内的代码编译器和CPU仍然可以重排。

    从可见性角度分析内存模型的order,为了描述多线程之间的代码之间的顺序关系和内存可见性关系。我们用happens before的语义表示,两行代码之间的关系。编译器和CPU的单线程内的优化是按照规则的,优化前的happens before的关系不会被打乱。这也是我们如果是单线程,就无需考虑代码重排的问题。因为编译器和CPU保证语义的正确。我们用happens before来表示我们期待的代码顺序关系。

    void func(){
        int i=2;   //1
        int j=4;   //2
        int s=i+j;   //3
    }

    1,2 之间没有happen before联系。大家都不依赖对方,所以可以重排。但是3绝对不可能重排到1,2 之前,因为1,2 happens before 3. 这是编译器单线程优化的规则。happen  before不仅仅是顺序,而是可见性。也就是1,2的结果,肯定在3之前就能被step 3感知到。

    为什么要强调可见性呢?因子在单线程中,同一个内存read的结果肯定相同,但是在多线程中因为cache,是可能不相同的。那么我们在多线程中,为了表示我们期待的执行结果。我们也用happens before表示。因为执行顺序不代表可见性。

    bool flag =false;
    int y = 0;
    // run at thread 1
    void M1() {
        y = 42;           // 1
        flag= true;            // 2
    }
    // run at thread 2
    void M2() {
        while (!flag);   //3
        y++;              //4
     
    }

    多线程之间我们可以认为的规定happens before语义, 如此就能保证正确的结果。那么如何保证 1,2 happens before 3呢,memory model。

    既然知道多线程同步的难点,那么看看C++11提供了哪些内存模型:都是基于原子的操作

    typedef enum memory_order
    {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
    } memory_order;

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

  • 相关阅读:
    BTC比特币全节点部署
    redis缓存层实现redission
    thinkphp5日志文件权限的问题
    MYSQL查两个经纬度之间的直线距离,计算出的单位:米
    thinkphp 建单独的日志目录
    VSCode远程连接Linux服务器
    Linux smb 的挂载和取消挂载及解决类似umount target is busy挂载盘卸载不掉问题
    System limit for number of file watchers reached
    JavaScript 字符串方法
    Linux下常用压缩 解压命令和压缩比率对比
  • 原文地址:https://www.cnblogs.com/kkshaq/p/11245721.html
Copyright © 2020-2023  润新知