为了说明互斥锁的饥饿状况,我将以 拉斯·考克斯 ( Russ Cox)提出的 关于他们讨论互斥锁改进的 问题 为例:
starvation.go
此示例基于两个goroutine:
两者都具有100微秒的周期,但是由于goroutine 1一直在请求锁定,因此可以预期它将更频繁地获得锁定。
这是一个用Go 1.8进行的示例,该示例具有10次迭代的循环的锁分配:
该互斥锁已被第二个goroutine捕获了十次,而第一个则超过了700万次。让我们分析一下这里发生了什么。
首先,goroutine 1将获得锁定并睡眠100微秒。当goroutine 2尝试获取锁时,它将被添加到锁的队列(FIFO顺序)中,并且goroutine将进入等待状态:
Figure 1 — lock acquisition
然后,当goroutine 1完成工作时,它将释放锁定。此版本将通知队列唤醒goroutine 2。Goroutine 2将被标记为可运行,并正在等待Go Scheduler在线程上运行:
Figure 2— goroutine 2 is awoke
但是,在goroutine 2等待运行时,goroutine 1将再次请求锁定:
Figure 3— goroutine 2 is waiting to run
当goroutine 2尝试获取锁时,它将看到它已经具有保持状态并进入等待模式,如图2所示:
Figure 4— goroutine 2 tries again to get the lock
goroutine 2对锁的获取将取决于它在线程上运行所花费的时间。
现在已经确定了问题,让我们回顾可能的解决方案。
处理互斥量的方法有很多,例如:
barging mode
Go 1.8就是这样设计的,它反映了我们之前看到的内容。
handoff mode
我们可以 在Linux内核 的 互斥体中 找到此逻辑:
在我们的情况下,互斥锁切换会完美平衡两个goroutine之间的锁分配,但是会降低性能,因为这将迫使第一个goroutine即使未持有也要等待锁。
spinning mode
Go 1.8也使用此策略。当试图获取已经持有的锁时,如果本地队列为空且处理器数量大于一,则goroutine将旋转几次-如果仅使用一个处理器旋转就会阻塞程序。旋转后,goroutine将停放。如果程序大量使用锁,它可以作为快速路径。
有关如何设计锁的更多信息( 插入 ,越区切换,自旋锁),通常, Filip Pizlo撰写 了必读的文章“WebKit中的锁定 ”。
在Go 1.9之前,Go结合了插入和旋转模式。在1.9版中,Go通过添加新的饥饿模式解决了先前解释的问题,该模式将导致在解锁模式期间进行切换。
所有等待锁定时间超过一毫秒的goroutine,也称为 有界等待 ,将被标记为饥饿。当标记为饥饿时,解锁方法现在将把锁直接移交给第一位服务员。这是工作流程:
starvation mode
由于进入的goroutine将不会获取任何为下一个服务员保留的锁,因此在饥饿模式下也将禁用旋转。
让我们使用Go 1.9和新的starvation模式运行前面的示例:
现在的结果更加公平。现在,我们想知道新的控制层是否会对互斥体不处于饥饿状态的其他情况产生影响。正如我们在该程序包的基准测试(Go 1.8与Go 1.9)中所看到的,在其他情况下,性能并没有下降(不同处理器数量下, 性能会略有变化 ):
翻译自: https://medium.com/a-journey-with-go/go-mutex-and-starvation-3f4f4e75ad50