同步的底层原理
软件同步
所谓的锁,可以理解为内存中的一个整型数。它拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。
硬件同步
在硬件方面,CPU提供了原子操作、关闭中断(单核且非抢占内核)、锁内存总线。
CPU会通过对内存总线加锁的手段来解决多核同时获取锁的情况,它是怎么实现的呢?
在CPU芯片上有一个HLOCK Pin,可以通过发送指令来操作,将#HLOCK Pin电位拉低,并持续到这条指令执行完毕,从而将内存总线锁住,这样同一总线上的其他CPU就不能通过总线来访问内存了。
最开始这些功能是用来测试CPU的,后来被操作系统实现而封装成各种功能:同步代码块,信号量等。
简单的关闭中断在多核CPU环境下是行不通的。需硬件提供支持,硬件提供的原语操作:
test and set --原子操作
返回ptr指向的旧值,同时ptr的值更新为new。
// 伪代码
int TestAndSet(int *ptr, int new) {
int old = *ptr;
*ptr = new;
return old;
}
// 自旋锁实现伪代码
typedef struct __lock_t {
int flag;
} lock_t;
void init(lock_t *lock) {
lock->flag = 0;
}
void lock(lock_t *lock) {
while (TestAndSet(&lock->flag, 1) == 1) {
; // spin-wait (do nothing)
}
}
void unlock(lock_t *lock) {
lock->flag = 0;
}
compare-and-swap --原子操作
检测ptr指定地址的值是否与expected相等;如果相等,就将ptr指向的内存地址更新为new值。如果不等的话,什么都不做。最后都返回actual值,从而可以让调用compare-and-swap指令的代码知道成功与否。
// 伪代码
int CompareAndSwap(int *ptr, int expected, int new) {
int actual = *ptr;
if (actual == expected) {
*ptr = new;
}
return actual;
}
// 自旋锁实现伪代码
typedef struct __lock_t {
int flag;
} lock_t;
void init(lock_t *lock) {
// 0 indicates that lock is available, 1 that it is held
lock->flag = 0;
}
void lock(lock_t *lock) {
while (CompareAndSwap(&lock->flag, 0, 1) == 1) {
; // spin-wait (do nothing)
}
}
void unlock(lock_t *lock) {
lock->flag = 0;
}
fetch-and-add --原子操作
原子地将某一地址的值加1。它保证了所有线程的执行,一旦某个线程得到了他自己的ticket值,在将来的某一时刻肯定会被调度执行(一旦前面的那些线程执行完临界区并释放锁)。
// 伪代码
int FetchAndAdd(int *ptr) {
int old = *ptr;
*ptr = old + 1;
return old;
}
// 排队锁实现伪代码
// 全局共享变量lock->turn用来决定轮到了哪个线程;
// 当对于某个线程myturn等于turn时,那就轮到了这个线程进入临界区。
// unlock简单地将turn值加1,由此下一个等待线程(如果存在的话)就可以进入临界区了。
typedef struct __lock_t {
int ticket;
int turn;
} lock_t;
void lock_init(lock_t *lock) {
lock->ticket = 0;
lock->turn = 0;
}
void lock(lock_t *lock) {
int myturn = FetchAndAdd(&lock->ticket);
while (lock->turn != myturn) {
; // spin
}
}
void unlock(lock_t *lock) {
FetchAndAdd(&lock->turn);
}
OS的上层的同步手段都基于此机制