并发原理—如何保证多条指令的原子性(二)
一、提出问题:一条CPU原语指令如何保证多条指令的原子性
从上篇文章我们知道当多个CPU访问(此处访问的含义不仅有读取内存数据的意思,同时也有往内存写入数据的意思)同一个数据时,CPU存在着穿插执行的行为,从而造成数据紊乱的情况,为此CPU提供了锁机制来保证数据一致性,锁机制背后的原理就是通过CPU的一条原子性指令(原语)限制只能有一个CPU执行该指令。但我们的应用程序的某个方法往往是由多条指令(通过汇编器产生的ISA指令集)组成的,那一条原语指令如何保证多条指令的原子性呢?换个问法就是:应用程序的多个线程访问同一个方法时是如何保证线程安全的呢?
二、只有设置标志位成功的线程才可以执行方法
如图所示,应用程序有一个方法,该方法内部存在对数据的写操作,现在有n个线程想同时执行该方法,那么势必会存在多线程并发的问题。那么CPU底层是如何通过一条CPU原语指令保证多条指令的原子性呢?答案就是通过引入一个标志位,让CPU去争抢设置标志位。这些CPU通过原子性指令(lock;add或者lock;sub)来设置标志位,由于是原子操作,那么只有一个CPU能设置成功,也即只有设置标志位成功的线程可以执行方法代码,设置失败的其他线程则要排队进入阻塞队列。
三、设置标志位失败的线程自旋进入阻塞队列
因为设置标志位失败的线程有多个,它们进入队列的过程是要拿到队尾指针,然后把队尾指针指向自己。那么抢这个队尾指针的操作,又是一个原子性操作,通过CPU的原子性指令(lock;cmpxchg)来让多个线程排队进入队列,这个操作称之为自旋。
四、唤醒和排队也是原子性操作
当线程执行完代码后,需要唤醒阻塞队列中的线程继续执行方法,而在唤醒的同时有可能有其他线程在排队进入队列中,所以唤醒和排队这两个操作也是原子性操作。
五、总结
我们绘制完整执行流程图,可以清晰看到CPU是如何通过一条CPU原语保证多条指令的原子性。我们可以通过以下伪代码简要概述上述逻辑过程:
if(设置标志位){
// 设置标志位成功的线程执行方法
} else {
// 设置失败的线程排队
for(;;){
// 通过CAS进入队列
}
}