• 自旋锁与互斥锁


    前言

    在编程中经常需要使用到互斥. 互斥就是, 这个事情只能有一个人干, 我正在做着的时候, 别人要想做这件事就得等我做完了.

    互斥的实现是通过锁的机制, 也就是我把这块锁上了, 别人就进不来了, 等我做完再把锁释放掉.

    但是, 前辈们已经证明了, 要想单纯的在软件层面上实现锁的机制是很难的, 即使是简单的一条加1的操作, 在CPU执行时也需要如下几步:

    1. 将变量从内存读到寄存器
    2. 寄存器中的值加1
    3. 将寄存器中的值写回内存

    而中间任何一步发生切换, 都可能导致锁机制的失败. 因此, 在软件层面的实现代价是很高的(感兴趣的可搜一下: Peterson 算法).

    造成其困难的原因是什么呢? 因为无法保证单条 CPU 指令的原子性. 既然软件不够, 那就硬件来凑咯.

    于是, CPU提供了lock指令, 可以保证单条指令的原子性. 而有了硬件的支持,锁的实现就简单的多了. 比如在有一条指令xchg用来对两个变量进行交换, 那么就可以将锁放到一个全局变量中, 规定谁换到锁了就持有, 用完再放回去. 很简单的实现了锁的机制. (至于硬件上是如何实现的, 我确实不甚了解, 因此这里按下不表)

    好, 现在能够很容易的实现锁了, 但是既然能拿到锁, 那也就有可能拿不到锁. 如果没有拿到锁, 怎么办呢? 两种应对方案既: 自旋/互斥, 他们也是其他锁(读写锁/乐观锁/悲观锁)的底层实现.

    自旋锁

    在获取锁失败的情况下, 立刻再次尝试获取. 大概这样:

    int locked = 0;
    void lock() {
        // 将1放入 locked 变量
        // 若 locked 中存放的是 1, 则说明当前已经有其他人获取了, 继续等待
        while (xchg(&locked, 1)) ;
    }
    void unlock() {
        // 使用完后, 将变量置为0
        xchg(&locked, 0);
    }
    

    既, 线程会不停的尝试获取锁.

    但是, 忙等会导致如下问题:

    • 线程在空转, 浪费 CPU 性能
    • 争抢锁的线程越多, CPU 的利用率越低
      • 假设有100个线程在争抢锁, 其中一个线程拿到了, 那么剩下99个都在空转
      • 如果抢到锁的线程被操作系统调度走了, 那么剩下的全部线程(99个)都在空转

    互斥锁

    自旋锁的问题其实就出在忙等上, 假设没有拿到锁的话, 就将线程暂时休眠, 等到锁被释放了再将其唤醒, 这样不就能够避免性能的浪费了嘛.

    但是, 线程的调度靠线程自己是无法完成的. 需要操作系统帮忙调度.

    既然进行了线程调度, 那必然就需要进行线程的上下文切换了.

    但是, 如果锁的占用时间比线程上下文切换的时间还要短呢? 这边线程上下文切换还没完成, 那边锁已经释放了, 这不就会导致运行效率的降低了么.

    结合

    自旋锁的问题是忙等会浪费 CPU 性能, 而互斥锁的问题是若锁的持有时间极短会导致运行效率的降低.

    也就是说

    • 在占有锁的时间较短时, 自旋锁的开销更小. (可以立即获取锁)
    • 在占有锁的时间较长时, 互斥锁的开销更小. (CPU 不用空转)

    那么有没有一种既不会浪费 CPU 性能, 又不会降低线程运行效率的办法呢? 有,

    1. 通过自旋尝试获取锁
    2. 若获取失败, 则转为互斥

    这样可以令大部分情况在首次获取锁时便能拿到, 无需线程切换. 在较少的情况下, 会造成部分性能的浪费. 但是整体性能是提高了的.

    最后, 我们在日常上层开发的时候, 其实很少考虑获取锁的实现方式是自旋还是互斥, 更多考虑的是读写锁还是什么. 底层已经为我们选择了最合适的方式.

  • 相关阅读:
    Mysql主从同步延迟问题及解决方案
    elasticsearch 查询过程
    RPC(Remote Procedure Call):远程过程调用
    windows
    设计模式
    Linux Safe
    AS
    开机启动
    springboot打包部署
    【Linux】Linux 常用命令汇总
  • 原文地址:https://www.cnblogs.com/hujingnb/p/16220962.html
Copyright © 2020-2023  润新知