今天无聊看了看<程序员的自我修养>,才明白原来线程安全还别有洞天,它总是一个棘手的问题..即便我们已经用了原子操作,信号量,互斥量,临界区等等手段尽力保证线程的同步,也不一定能保证线程安全.这是由于落后的编译器技术已经无法满足日益增长的并发需求,以致于很多看似无错的代码在优化和并发面前又产生了麻烦..
过度优化
为了说明这个问题,<程序员的自我修养>举了一个很简单的例子:
...
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++的操作在机器执行起来,变成了这么三步:
- 取i值到寄存器
- 寄存器值加1
- 寄存器值写回给i
而不同线程的寄存器是各自独立的,有一种可能导致global_x值不为2的情形如下:
- [Thread1] 读global_x值到寄存器R[1]
- [Thread1] R[1]++ (R[1]=1)
- [Thread2] 读global_x值到寄存器R[2]
- [Thread2] R[2]++ (R[2]=1)
- [Thread2] 将寄存器R[2]的值写回global_x (global_x=1)
- [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基本可以做到这么两件事:
- 阻止编译器为了提高速度将一个变量缓存在寄存器中而步写回
- 阻止编译器调整操作volatile变量的指令顺序
悲剧的是,volatile虽然能够阻止编译器调整顺序,却无法阻止CPU动态调度换序.这使得我们为了线程安全的努力变得异常困难..
从上面的讨论中,我们可以知道,为了保证线程安全,阻止CPU换序是必需的.虽然各种体系结构的CPU都提供了一个barrier指令,用于阻止CPU将该指令之前的指令交换到barrier之后,但这却涉及到了平台的问题,也就意味着,我们写出来的程序不再是可移植的了,这不得不说是一个遗憾..
有一个地方不理解:
1.[Thread1] 读global_x值到寄存器R[1]
2.[Thread1] R[1]++ (R[1]=1) ———-这一步执行完了以后,Thread1的锁并没有释放。
3.[Thread2] 读global_x值到寄存器R[2]———-线程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)