线程安全

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

原子操作

典型的例子就是++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;
}
原文地址:https://www.cnblogs.com/xzlq/p/8795103.html