自适应自旋锁–吞吐量和延迟以及管理开销的折中_自适应锁 什么时候可以用

很多时候,当一个进程为了等待mutex而刚刚进入睡眠的时候,mutex已经被释放了,如果能在第一时间感知mutex被释放那是再好不过的了,解决该问题的方式就是用自旋忙等而不是阻塞等待,是这样吗?
关于竞态,当初由于不能忍受频繁的睡眠/唤醒而引入了自旋锁,然后又因为自旋锁在实时系统中会导致其它进程长时间延迟造成吞吐量下降而在实时系统中又恢复 了睡眠/唤醒,实时系统中的首要特性不是节省开销,而是保证运行,睡眠/唤醒机制保证了争抢锁的进程不会影响到别的进程从而保证了吞吐量不会被降低,但是 自旋锁用mutex实现的代价是客观存在的,它必须实现优先级继承或者优先级置顶以保证不会发生优先级逆转和死锁,因为现在自旋锁可以睡眠了,而内核又是 可以抢占的,一旦有高优先级的进程就绪,那么它就会抢占低优先级的进程,这是如果低优先级的进程正好持有那一把锁,那么就会被阻塞而不能运行(单cpu或 者进程/cpu绑定的情况),此时一个中间优先级的进程抢占了低优先级的进程,那么这会导致高优先级的进程的阻塞时间过于长,在实时系统中必须避免这种情 况,因此在实时系统内核中,比如linux内核中,就实现了优先级继承协议-PIP,协议的实现引入了大量的数据结构和算法,这就引入了管理开销,这些开 销会抵消掉一部分睡眠/唤醒带来的吞吐量的提高和其它进程延迟的降低,因此,下面的一杆秤就需要在这二者之间拨弄秤砣了。
传统自旋锁的实现保证了自旋锁的持有者不会被打断,这就可以保证即使高优先级的进程被破延迟也只是在自旋,而自旋时间内持有锁的进程会一直运行,而且运行 逻辑一直和这把锁有关,即使它释放了锁之后没有被等待者抢到,那么抢到锁的进程也不会做别的事(在ticket spin lock中这种混乱得到了改善),虽然延迟是有的,但是起码都是在围绕着锁作正经事而不会被别的进程打断,如果你真的在持有自旋锁的时候调用了一个 schedule,那么只能怪写代码的人了。因此传统的自旋锁不用任何开销就可以避免优先级逆转之类的令人难堪的局面,是的,没有任何开销,把抢占一关锁 一占完事,剩下的就尽情执行吧,不会被打扰,然而这样的话虽然避免了优先级逆转带来的争抢锁高优先级的进程延迟但是会引入不争抢锁的所有高优先级进程的延 迟,因为自旋锁简单的关闭了抢占(简单无开销)。因此传统的自旋锁和睡眠/唤醒机制都不适合实时系统,带有优先级继承协议的mutex机制实现的自旋锁是 可以的,但是管理优先级继承协议的数据结构和算法也够呛,这么多的但是降到底如何是好,现在有三种方案实现实时系统自旋锁,第一就是传统自旋锁,第二就是 传统的睡眠/唤醒机制,第三就是实现PIP的mutex机制,前两种都基本没有管理开销但是因为影响系统吞吐量和延迟,第二种还会引入睡眠/唤醒开销,这 导致这两种不能用,第三个方案由于不怎么影响系统延迟但是运行开销很大,运行开销有睡眠/唤醒的开销,管理复杂数据结构和算法的开销。因此,将这三种方案 的优势组合然后避免它们的劣势是最好的结果了,其实这三种方案正交化一下就是:自旋锁,睡眠/唤醒,PIP协议,其中PIP是不可省略的,因此选择自旋锁 的低开销和睡眠/唤醒的高吞吐量,因此结果就是自适应自旋锁,也就是说它会在某些情况下自旋而在另外一些情况下睡眠,这就是它的优势。那么在何种情况下自 旋呢?理想的情况就是自动学习,起初可能会很影响性能,毕竟要学习嘛,多次尝试自旋以后,会得到一些统计值,比如得到锁的平均延迟,平均自旋次数,n次自 旋内得到锁的次数,系统根据这些统计值决定下一次是自旋还是睡眠。然而这种实现合理吗?看似很合理啊,也很智能,几乎不用怎么配置就可以自动运行的很好, 但是想想看,这毕竟是在内核,内核不是秀算法的地方,智能的,复杂的算法还是在用户空间秀比较好,内核算法和数据结构的特征就是简单,高效,因此内核实现 的自适应锁还是要另外开辟新的方案。
     linux的内核高效的原因之一就是它采用了约定的方式来约束开发,而不是强制的方式在运行时验证,这样会节省很大的管理开销,在linux中,进大门的 人很自觉,坏人一般不会进入,内核信任能进入的都是好人,因此就节省了两个门卫,同时也节省了查证的时间,linux为何如此信任代码呢?因为linux 内核是开源的,任何不合法的东西都逃不过开发者的慧眼。静态的东西总是比动态的更加高效,因为最起码它节省了计算的时间,那么对于自适应锁,linux是 怎么约定的呢?其实只要是锁,linux都会建议拥有者尽量赶快完毕和锁相关的事然后释放掉锁,毕竟锁是公用的,因此linux内核相信所有的代码都接受 了这个建议约定,因此,一个持有锁的执行绪会在最快的时间内放掉锁,仅此一点还不能让争抢锁的执行绪为之自旋,mutex实现的自旋锁是可以被抢占的,当 然还是不建议主动睡眠,mutex实现的自旋锁可睡眠可被抢占只是为了实时,为了高优先级的进程不自旋可以马上抢占它,它可不是睡眠的温床,因此,不要在 mutex自旋锁中睡眠。因为持有锁的进程可能会被抢占,那么在这种情况下争抢锁的进程自旋是没有多大几率成功获得锁的,因为会涉及到很复杂的操作,比如 被抢占的进程可能被调度到别的cpu上,但是不知道是哪个cpu上,真的被调度到别的cpu上了吗?也就是它真的正在运行吗(努力执行释放锁)?不得而 知,因此补丁中会有如下判定:
while (waiter->spin && !lock->waiters) {
    struct thread_info *owner;
    owner = ACCESS_ONCE(lock->owner);  //在这个owner上自旋
    if (owner && !mutex_spin_on_owner(waiter, owner))
        break;
    cpu_relax(); //自旋
}
以下是mutex_spin_on_owner的逻辑
while (waiter->spin && !lock->waiters) {
    if (lock->owner != owner) //owner换了,重新开始判定,进入上一层自旋
        break;
    if (task_thread_info(rq->curr) != owner) { //锁的owner没有运行在owner原来的cpu上,不再自旋
        ret = 0;
        break;
    }
    if (need_resched()) { //争抢锁的cpu上有更高优先级的进程就绪,不再自旋
        ret = 0;
        break;
    }
    cpu_relax(); //自旋
}
我们看到有两层的自旋,外层的自旋是为了在不同owner中争抢锁,因为一个owner把锁释放了,并不代表我们就是下一个得到锁的,所以需要外层的自 旋,内层的自旋才是真正的自旋。其实实现自适应自旋锁的原始方式就是自旋counter次,然后睡眠,这不过是一种简单的实现方式罢了,没有添加任何的策 略,没有跟踪锁的owner的情况。
可以看出,系统设计中充满了矛盾,关键不是解除矛盾,矛盾是解除不了的,解除矛盾意味着功能的缺失,这也比较符合我们的世界,怕翻车就走路...设计当中 最最关键的就是权衡矛盾,最后得到最好的折中,比如时间和空间就是一对矛盾,用时间换空间还是用空间换时间取决于你的应用,另外,对于我今天讨论的自旋锁 的实现,延迟和吞吐量以及管理开销也是一对矛盾,传统的自旋锁吞吐量小,但是造成其他进程延迟,而睡眠/唤醒吞吐量大,也不会影响太多的延迟,但是开销过 于大,因此必须做好权衡,取它们的优点和缺点作比较。在自适应自旋锁的设计中,吞吐量和延迟的矛盾不在自旋造成的吞吐量下降和睡眠/唤醒本身的延迟,而在于自适应锁带来的别的进程的延迟减少和管理复杂数据结构的开销造成的吞吐量下降之间的矛盾。

原文链接: https://blog.csdn.net/dog250/article/details/5302851

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    自适应自旋锁--吞吐量和延迟以及管理开销的折中_自适应锁 什么时候可以用

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/410665

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 下午12:09
下一篇 2023年4月26日 下午12:09

相关推荐