zoukankan      html  css  js  c++  java
  • 线程安全

    最近在看《程序员的自我修养》,做一下笔记。

    原子操作

    典型的例子就是++i这种,看着像是一条语句,其实编译器会把它翻译成多条执行命令,让操作系统执行。i++的汇编语句执行过程:

    1) 读取i到某个寄存器X

    2) X++

    3) 将X的内容存储回i。

    所以两个线程同时操作i的时候,会出现交叉赋值的情况,使执行结果变得未知。

    我们把汇编语句层面的单条指令的操作成为原子。

    在Windows中,有一套API专门进行一些原子操作,这些API成为Interlocked API。

    编写可重入函数,保证线程安全

    一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行。一个函数要被重入,只有两种情况:

    1)多个线程同时执行这个函数

    2) 函数自身调用自身

    一个函数被称为可重入,表明该函数被重用之后不会产生任何不良后果。举例:

    int sqr(int i)
    {
       return i*i;
    }
    

     可重入函数的特点:

    1) 不使用任何(局部)静态或全局的非const变量。

    2) 不返回任何(局部)静态或全局的非const变量的指针。

    3) 仅依赖于调用方提供的参数,

    4) 不依赖任何单个的资源的锁

    5) 不调用任何不可重入的函数

    可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用

    过度优化

    过度优化不是指我们自己优化过度。而是指编译器或者操作系统帮我们进行偷偷的优化,导致我们的程序在多线程下出现一些怪异的情况。

    x = 0
    
    Thread1                  Thread2
    lock()                   lock()
    x++;                     x++;
    unlock()                 unlock()
    

     上面提到过的,现在用锁给保护了,X++的行为不会被并发所破坏。那么X的值必然可以预测。

    然而,如果编译器为了提高X的访问速度,把X放到某个寄存器里,由于不同线程的寄存器是各自独立的。因此如果Thread1先获得锁,则程序的执行可能会出现如下的情况:

    可见这样的情况下即使正确的加锁,也不能保证多线程安全。

    例子2

    x=y=0
    
    Thread1            Thread2
    x=1;               y=1;
    r1=y;              r2=x;
                  
    

     很显然,r1和r2至少有一个为1,逻辑上不可能同时为0.

    然而,事实上r1=r2=0的情况确实可能发生。原因在于十几年前,CPU就发展出了动态调度,在执行程序的时候为了提高效率可能交换指令的顺序同样,编译器在进行优化的时候,也可能为了效率而交换毫不相干的两条相邻执行(如x=1和r1=y)的执行顺序。以上代码执行的时候可能是这样的:

    x=y=0;
    
    Thread1            Thread2
    r1=y;              y=1;
    x=1;               r2=x;
    

     那么r1=r2=0就完全可能了。我们可以使用volatile关键字试图阻止过度优化,volatile基本可以做到两件事情:

    1)阻止编译器为了提高速度将一个变量缓存到寄存器内而不回写。

    2) 阻止编译器调整操作volatile变量的指令顺序。

    可见volatile可以解决编译器层面的顺序调整问题,但是无法阻止CPU动态调度换序

    例3

    单例模式的实现 https://www.cnblogs.com/myd620/p/6133420.html

    volatile T *pInst = 0;
    T *getInstance()
    {
        if(pInst == NULL)
       {
            lock();
            if(pInst == NULL)
            {
                pInst = new T;
            }
           unlock();
       }
      return pInst;
    }

    上面代码双重if在这里另有妙用,可以让lock的调用开销降低到最小。

    问题剖析

    问题来源仍然是CPU的乱序执行。

    C++里的new其实包含两个步骤:

    1)  分配内存

    2)调用构造函数

    所以pInst = new T包含了三个步骤:

    1) 分配内存

    2)在内存的位置上调用构造函数

    3) 将内存的地址赋值给pInst

    在这三步中,2)和3)是可以颠倒的。也就是说,完全可以出现pInst的值已经不是NULL, 但对象仍然没有构造完毕,这时候如果出现另外一个对getInstance的并发调用,此时第一个if内的表达式pInst==NULL为false,会返回一个为构造对象的地址给用户,当然了,程序就崩溃了。

    如何解决这个问题呢?

    volatile T *pInst = 0;
    T *getInstance()
    {
        if(pInst == NULL)
       {
            lock();
            if(pInst == NULL)
            {
                T* temp = new T;
                pInst  = temp;
            }
           unlock();
       }
      return pInst;
    }
  • 相关阅读:
    三种按键处理函数
    enum与typedef enum的用法
    PIC18F中断定时器
    .net core Ocelot+Consul实现网关及服务注册和服务发现
    wpf的优点
    ASP.NET Core Web API下事件驱动型架构的实现
    2020个人计划
    图解C#的值类型,引用类型,栈,堆,ref,out
    .NET Core中的一个接口多种实现的依赖注入与动态选择
    redis分布式锁深度剖析
  • 原文地址:https://www.cnblogs.com/xzlq/p/8795103.html
Copyright © 2011-2022 走看看