今天无聊看了看<程序员的自我修养>,才明白原来线程安全还别有洞天,它总是一个棘手的问题..即便我们已经用了原子操作,信号量,互斥量,临界区等等手段尽力保证线程的同步,也不一定能保证线程安全.这是由于落后的编译器技术已经无法满足日益增长的并发需求,以致于很多看似无错的代码在优化和并发面前又产生了麻烦..

过度优化

为了说明这个问题,<程序员的自我修养>举了一个很简单的例子:

...

int global_x = 0; // 两个线程共享的全局变量.

Thread1:  // 线程1的定义                                                           Thread2: // 线程2的定义

lock();                                                                                               lock();

global_x ++;                                                                                   global_x ++;

unlock();                                                                                          unlock();

...

看上去,因为线程1和线程2在访问global_x时都使用了lock()和unlock()保护,因此global_x ++ 的行为不会被并发破坏,所以在线程1和线程2结束之后,global_x 的值似乎一定是2..但其实,这么理所当然的猜测有可能是错误的.解释如下:

我们知道,一个简单的i=i++的操作在机器执行起来,变成了这么三步:

  1. 取i值到寄存器
  2. 寄存器值加1
  3. 寄存器值写回给i

而不同线程的寄存器是各自独立的,有一种可能导致global_x值不为2的情形如下:

  1. [Thread1] 读global_x值到寄存器R[1]
  2. [Thread1] R[1]++ (R[1]=1)
  3. [Thread2] 读global_x值到寄存器R[2]
  4. [Thread2] R[2]++ (R[2]=1)
  5. [Thread2] 将寄存器R[2]的值写回global_x (global_x=1)
  6. [Thread1] 将寄存器R[2]的值写回global_x (global_x=1)

之所以会出现这样的问题,是因为编译器为了提高global_x的访问速度,将global_x的值放到了某个寄存器里.,这就导致了所谓过度优化的问题..<程序员的自我修养>中还举了一个简单的例子,说的是CPU在执行程序的时候,为了提高效率可能交换指令的执行顺序..同样的,编译器在优化时,也可能会交换毫不相关的两条相邻指令..比如说:

x = y = 0;

x = 1;

z = y;

也许就会被调换成:

x = y = 0;

z = y;

x = 1;

从第1个例子可见,过度优化在单线程时不会有什么问题,而在多线程中可能就将导致一场灾难了..

解决过度优化

为了阻止过度优化,我们可以使用volatile关键字,volatile基本可以做到这么两件事:

  1. 阻止编译器为了提高速度将一个变量缓存在寄存器中而步写回
  2. 阻止编译器调整操作volatile变量的指令顺序

悲剧的是,volatile虽然能够阻止编译器调整顺序,却无法阻止CPU动态调度换序.这使得我们为了线程安全的努力变得异常困难..

从上面的讨论中,我们可以知道,为了保证线程安全,阻止CPU换序是必需的.虽然各种体系结构的CPU都提供了一个barrier指令,用于阻止CPU将该指令之前的指令交换到barrier之后,但这却涉及到了平台的问题,也就意味着,我们写出来的程序不再是可移植的了,这不得不说是一个遗憾..