并发问题中,单机下多个进程同时访问某公共变量要实现临界区的互斥访问,不然容易发生冲突,而在集群状态下为了保证各个节点的数据一致性也需要进行同步,实现同步一般都通过锁机制(包括信号量)来完成。其中底层有两个基本的锁,就是互斥锁(Mutex Lock)和自旋锁(Spinlock),单机环境下大多情况都可以通过这两个锁来进行同步
1 锁的本质
锁,在计算机里本质上就是一块内存空间。当这个空间被赋值为1的时候表示加锁,被赋值为0的时候表示解锁。多个线程抢一个锁,就是抢着要把这块内存赋值为1。在多核环境中,内存空间是共享的,如果每个核上跑一个线程,要保证一次只有一个线程抢到锁,则需要使硬件实现缓存一致性协议
2 硬件实现
锁的硬件实现依赖底层的硬件指令,TAS(Test And Set)和 CAS(Compare And Set)
2.1 TAS指令
TAS指令的语义是:向某个内存地址写入值1,并且返回这块内存地址存的原始值。TAS指令是原子的,这是由实现TAS指令的硬件保证的
伪代码实现:
function TestAndSet(boolean lock) {
boolean initial = lock;
lock = true;
return initial;
}
在x86架构中,TAS对应的汇编指令是bts(bit test and set),在多核CPU中,需要在前面加lock前缀,也就是lock bts
2.2 CAS指令
CAS的语义是:比较某个内存地址的值与一个给定值(这个给定值是上一刻从此内存地址读出来的),如果相等,则把一个新值写入到此内存地址
伪代码实现:
int compare_and_swap(int* reg, int oldval, int newval)
{
ATOMIC();
int old_reg_val = *reg;
if (old_reg_val == oldval)
*reg = newval;
END_ATOMIC();
return old_reg_val;
}
在x86架构中,CAS对应的汇编指令是CMPXCHG,也是一种原子操作指令
3 软件实现
如图,使用加锁来实现互斥和同步的一般模型包含lock(),和unlock()
当执行加锁函数lock(lock)时,如果判断到lock已被加锁,那么有两种情况,一种会调用系统函数将当前线程阻塞,还有一种是一直循环处于判断lock状态,如果lock=0接着循环,如果lock=1跳出循环,转到临界区,这种就是自旋锁。这两种锁各有利弊,已加锁时阻塞掉当前线程,让出cup空出资源可以去执行别的线程,通过减少cup的浪费来提高效率,但是这个过程需要进行上下文切换,保存各寄存器状态需要花费时间,如果每个线程对于锁的占有时间很短,获取锁之后会很快就释放锁,那么自旋锁将会更加高效,所以要根据具体情况具体来使用。另外需要注意的是自旋锁只能用于多核cup,如果是单核将无限制处于自旋状态
3.1 互斥锁的变量能由用户定义吗?
上面提到,锁的本质就是控制内存空间的访问,对应到系统中来讲,引入二值性变量state,通过对这个state进行+1和-1进行加锁和解锁,那么需要考虑下这个变量能交给用户定义吗?
实际上是可以的,只要能保证lock,unlock函数满足以下两点就可以:
(1)对于lock的判断操作为原子性,要么全部执行,要么全部不执行
(2)一个线程访问lock的时候不允许另一个线程同时访问,这一点需要禁用中断,而且要禁用所有cup核的中断,即给总线加锁,防止其他核的线程访问到lock
但是它会存在一些问题,比如:
①如果是用户程序定义lock,那么lock变量只能此进程访问,这样无法来对不同进程的线程进行同步,极其不方便
②对于阻塞方式的加锁过程,当解锁的时候要判断还有哪些线程申请这个锁,然后将其从等待队列中拿出来放入就绪队列。如果lock是用户程序定义,那么操作系统将无法来判断到底哪些线程申请过这个锁,也将无法将对应的线程激活
因此,互斥锁lock变量必须由操作系统来定义,自旋锁可以用户程序定义
(4)注意死锁产生的条件
一起探索架构技术,拥抱AI,欢迎与我交流!