【第16期】浅谈Mutex与Spinlock

文摘   2024-09-24 07:46   广东  


    并发问题中,单机下多个进程同时访问某公共变量要实现临界区的互斥访问,不然容易发生冲突,而在集群状态下为了保证各个节点的数据一致性也需要进行同步,实现同步一般都通过锁机制(包括信号量)来完成。其中底层有两个基本的锁,就是互斥锁(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 Mutex和Spinlock 对比

Mutex:如果一个线程试图获取一个 mutex,但是没有成功,因为 mutex 已经被占用, 它将进入睡眠,让其他进程运行,直到 mutex 被其他进程释放

Spinlock:如果一个线程试图获取一个 Spinlock, 但是没有成功,它将持续试着去获取它,直到它最终成功获取,因为它将不允许其他线程运行(然而,操作系统将强制调度其他线程)


5 死锁(deadLock)

5.1死锁产生条件

(1)互斥
(2)请求与保持
(3)不可抢占锁资源
(4)循环等待

5.2 场景1:竞争资源


如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁;然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,,也就永远处于挂起等待状态了,这样就产生了死锁

5.3 场景2:线程获取资源顺序不当


线程A获得了锁1,线程B获得了锁2。这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,此时,线程A和线程B永远陷入等待,进入死锁

6 小结

(1)锁的硬件实现依赖底层的硬件指令,TAS(Test And Set)和 CAS(Compare And Set),这两者都是原子操作的指令

(2)自旋锁只能用于多核cup,如果是单核将无限制处于自旋状态

(3)互斥锁lock变量必须由操作系统来定义,自旋锁可以用户程序定义

(4)注意死锁产生的条件


一起探索架构技术,拥抱AI,欢迎与我交流!


顶尖架构师栈
大厂架构师,专注科技资讯,AI前沿信息,日常分享技术干货,程序员副业,职场三两事。
 最新文章