Go: 互斥锁和饥饿
ℹ️ 本文基于 Go 1.13。
在Golang中开发时,当互斥锁不断地试图获得一个永远无法获得的锁时,它可能会遇到饥饿问题。在本文中,我们将研究一个影响Go1.8的饥饿问题,该问题在Go1.9中得到了解决。
饥饿
为了说明使用互斥锁的饥饿情况,我们将从官方issue中关于互斥锁改进讨论中的例子开始:
此示例基于两个goroutine:
goroutine1 持有锁很长时间,并短暂地释放它
goroutine2 短暂地持有锁并释放它很长一段时间
两者都有100微秒的周期,但是由于goroutine1不断地请求锁,所以我们可以预期它将更频繁地获得锁。
下面是一个使用Go 1.8完成的锁分发的示例,循环为10次迭代:
互斥锁被第二个goroutine获得了10次,而第一个goroutine获得了700多万次。让我们分析一下这里发生了什么。
首先,goroutine1将获得锁并休眠100微秒。当goroutine 2试图获取锁时,它将被添加到锁的队列中(先入先出队列)并且goroutine将进入等待状态:
然后,当goroutine1完成它的工作时,它将释放锁。此释放将通知队列唤醒goroutine2。Goroutine 2将被标记为可运行,并等待调度到线程上运行:
但是,当goroutine 2等待运行时,goroutine 1将再次请求锁:
当goroutine2尝试获取锁时,它将看到锁已被其他goroutine获取,进入等待模式
goroutine2对锁的获取将取决于它在线程上运行所需的时间。
现在问题已经确定,让我们回顾一下可能的解决方案。
驳接 和 切换 和 自旋
有许多方法可以处理互斥锁,例如:
驳接. 这是为了提高吞吐量。当锁被释放时,它将唤醒第一个等待者,并将锁交给第一个传入请求的goroutine或此已唤醒的goroutine:
这就是Go1.8中的设计,它反映了我们之前看到的情况。
切换. 释放后,互斥锁将保持锁,直到第一个等待goroutine准备好获取它。这将降低吞吐量,因为即使有其他goroutine请求也无法获取到锁:
自旋. 当等待的队列为空或应用程序大量使用互斥锁时,自旋很有用。 停放和唤醒的成本很高,可能比仅自旋等待下一个锁的获取要慢:
Go 1.8也使用此策略。 当试图获取已经持有的锁时,如果本地队列为空且处理器数量大于1,则goroutine将自旋几次。如果仅使用一个处理器进行自旋只会阻塞程序。 自旋后,goroutine将停放。 如果程序大量使用锁,它可以作为快速路径。
饥饿模式
在Go 1.9之前,Go将驳接和自旋模式结合在一起。在版本1.9中,Go通过添加一个新的饥饿模式来解决先前的问题。
所有等待锁定超过一毫秒的goroutine,也称为有界等待,将被标记为饥饿。 当标记为饥饿时,解锁方法现在将把锁直接移交给第一位等待着。 这是工作流程:
由于进入的goroutine将不会获取任何为下一个等待者保留的锁,因此在饥饿模式下也将禁用自旋。
让我们使用Go 1.9和新的饥饿模式运行前面的示例:
现在的结果更加公平。 现在,我们想知道这个新的控制层是否会对互斥锁不处于饥饿状态的其他情况产生影响。 正如我们在该程序包的基准测试(Go 1.8 vs. 1.9)中看到的那样,在其他情况下性能并未下降(在不同数量的处理器下性能略有变化):
编译自https://medium.com/a-journey-with-go/go-mutex-and-starvation-3f4f4e75ad50
博客地址https://www.chenjie.info/2553
本文首发于我的公众号:
版权声明: 本文为 InfoQ 作者【陈思敏捷】的原创文章。
原文链接:【http://xie.infoq.cn/article/ab2a8a779cd7ef510adce957e】。文章转载请联系作者。
评论