在解决实际问题中经常会碰到各种并发问题,比如单机状态,多个进程同时访问某公共变量要进行临界区的互斥访问,不然容易发生冲突,或者集群状态下为了保证各个节点的数据一致性也需要进行同步,实现同步一般都通过锁机制(包括信号量)来完成。在最底层有两个基本的锁,就是互斥锁跟自旋锁,单机状态下大多情况都可以通过这两个锁来进行同步,这篇文章我对这两种基本锁的底层实现做一个总结。
利用加锁来实现互斥和同步的一般模型为:
实际上锁就是一个变量,通过这个变量的值来判断是不是已加锁,比如可以定义变量lock=1表示未被加锁,lock=0表示已加锁。那么加锁过程可以描述为:判断变量lock是否等于1,如果lock=1,则将lock改为0表示已加锁,然后进入临界区。这时别的线程也想进入临界区,一判断lock=0,那么将不被允许进入临界区,直到拥有锁的线程释放锁lock,即将lock改为1。
但是这样会存在几个问题:
- 线程a想要进入临界区,判断lock=1,可以进入临界区,此时在另一个cup核执行的线程b也判断lock=1,也会进入临界区,这样两个线程都会访问临界变量,发生冲突。
- 对于单核也会发生类似错误,比如线程a判断lock=1之后正好发生CPU中断,操作系统调度另一个线程b来执行,恰巧线程b要判断lock是否等于1,由于线程a只是判断lock=1却没来得及将lock置为1,线程b自然会通过lock=1进入临界区,不能保证两者之间的同步。
要解决这两个问题,需要保证两点即可:
- 对于lock的判断操作为原子性,要么全部执行,要么全部不执行。
- 一个线程访问lock的时候不允许另一个线程同时访问。
要做到这两点,只要禁用中断就可以,而且要禁用所有cup核的中断。这就是解决思路,但是实际并不是这么操作的,因为屏蔽中断会导致系统效率低下,实际上只要保证同一时刻只有一个线程在操作lock就可以了,要做到这一点只要给总线加锁,防止其他核的线程访问到lock。
当执行加锁函数lock(lock)时,如果判断到lock已被加锁,那么有两种情况,一种会调用系统函数将当前线程阻塞,还有一种是一直循环处于判断lock状态,如果lock=0接着循环,如果lock=1跳出循环转去临界区,顾名思义这种就是自旋锁。两者各有利弊,已加锁时阻塞掉当前线程让出cup资源可以去执行别的线程,通过减少cup的浪费来提高效率,但是这个过程需要进行上下文切换,保存各寄存器状态需要花费时间,如果每个线程对于锁的占有时间很短,拥有锁之后会很快释放锁,那么自旋锁又更加高效,所以要根据具体情况具体使用。另外值得一提的是自旋锁只能用于多核cup,如果是单核将无限制处于自旋状态。
前面提到锁本质是一个变量,那么有一个问题,这个变量可以用户程序自己定义吗?实际上是可以的,只要能保证lock(lock),unlock(lock)函数满足上面提到的要求就可以,但是它会存在一些不便,比如:
- 如果是用户程序定义lock,那么lock变量只能此进程访问,这样无法来对不同进程的线程进行同步,极其不方便。
- 对于阻塞方式的加锁过程,当解锁的时候要判断还有哪些线程申请这个锁,然后将其从等待队列中拿出来放入就绪队列。如果lock是用户程序定义,那么操作系统将无法来判断到底哪些线程申请过这个锁,也将无法将对应的线程激活。
所以,互斥锁lock变量必须由操作系统来定义,自旋锁可以用户程序定义。下面我们已Linux系统为例,总结一下互斥锁和自旋锁的实现原理。
Linux中对于锁的实现都是通过futex系统调用。futex由一块能够被多个进程共享的内存空间组成,保存在用户空间的共享内存中,这样对于futex的操作可以放到用户态来执行而不是在内核态,实际上futex的作用就是减少系统调用的次数来提高系统的性能。具体要了解futex这里推荐一篇博客
Futex设计与实现www.jianshu.com下面是互斥锁的实现过程(伪代码):
//要保证函数体为原子操作
mutex_lock(mutex) {
lock(bus); //给总线加锁
mutex = mutex - 1;
if(mutex != 0)
block()
else
success
unlock(bus);
}
mutex_unlock(mutex) {
lock(bus);
mutex = mutex + 1;
if (mutex != 1)
wakeup();
else
success
unlock(bus);
}
在谈自旋锁之前我们先了解一下 CAS操作,CAS(compare and set)是一个定义的函数,它的逻辑功能如下:
bool compare_and_set (int * ptr, int old_val, int new_val) {
if( *ptr == *old_val ) {
*ptr = new_val;
return true;
}
return false;
} //当内存value中的值是old_val则重新赋值为new_val,返回true
之所以说CAS操作是因为CAS是一个原子性操作,现在几乎所有的cup指令都支持CAS的原子性操作,比如X86下对应的就是CMPXCHG。下面就可以利用CAS来实现自旋锁了。
int owner = 0; // 1表示自旋锁已分配给某个线程,0表示自旋锁没有分配个任何线程,即处于释放状态
void spin_lock(int * owner ) {
while(!compare_and_set(owner, 0, 1) {
}
}//需要说明的一点是系统对comare_and_set的内核实现中包含对总线加锁
本文总结了一下单机状态下实现互斥锁,自旋锁的机制,但是在集群状态下为了实现各节点的同步就无法用这种锁,而应该是分布式锁。