educoder 使用线程锁(lock)实现线程同步_深入理解互斥锁 自旋锁的实现机制

9b180115fe8efe8b2d0efbb190d4c197.png

在解决实际问题中经常会碰到各种并发问题,比如单机状态,多个进程同时访问某公共变量要进行临界区的互斥访问,不然容易发生冲突,或者集群状态下为了保证各个节点的数据一致性也需要进行同步,实现同步一般都通过锁机制(包括信号量)来完成。在最底层有两个基本的锁,就是互斥锁跟自旋锁,单机状态下大多情况都可以通过这两个锁来进行同步,这篇文章我对这两种基本锁的底层实现做一个总结。

利用加锁来实现互斥和同步的一般模型为:

ed510ea39cf00633f6c32b3dcd6b63b1.png

实际上锁就是一个变量,通过这个变量的值来判断是不是已加锁,比如可以定义变量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
d6e716087a7e9dbfa9444179844bfa66.png

下面是互斥锁的实现过程(伪代码):

//要保证函数体为原子操作

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的内核实现中包含对总线加锁

本文总结了一下单机状态下实现互斥锁,自旋锁的机制,但是在集群状态下为了实现各节点的同步就无法用这种锁,而应该是分布式锁。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值