go 睡眠300毫秒_Go:互斥体和饥饿

f2014dc00aba53df90957415fe435213.png

在Golang中进行开发时,互斥锁在不断尝试获取永远无法获取的锁时会遇到饥饿问题。在本文中,我们将探讨影响Go 1.8的饥饿问题,该问题已在Go 1.9中解决。

饥饿

为了说明互斥锁的饥饿状况,我将以拉斯·考克斯(Russ Cox)提出的关于他们讨论互斥锁改进的问题为例:

func main() { done := make(chan bool, 1) var mu sync.Mutex // goroutine 1 go func() { for { select { case 

starvation.go

此示例基于两个goroutine:

  • goroutine 1长时间保持该锁并短暂释放它
  • goroutine 2暂时持有该锁并释放很长时间

两者都具有100微秒的周期,但是由于goroutine 1一直在请求锁定,因此可以预期它将更频繁地获得锁定。

这是一个用Go 1.8进行的示例,该示例具有10次迭代的循环的锁分配:

Lock acquired per goroutine:g1: 7200216g2: 10

该互斥锁已被第二个goroutine捕获了十次,而第一个则超过了700万次。让我们分析一下这里发生了什么。

首先,goroutine 1将获得锁定并睡眠100微秒。当goroutine 2尝试获取锁时,它将被添加到锁的队列(FIFO顺序)中,并且goroutine将进入等待状态:

62a2479facca7fa4129e29b0581e6300.png

Figure 1 — lock acquisition

然后,当goroutine 1完成工作时,它将释放锁定。此版本将通知队列唤醒goroutine 2。Goroutine 2将被标记为可运行,并正在等待Go Scheduler在线程上运行:

9e6252edbcea77c5198e83d7c1197163.png

Figure 2— goroutine 2 is awoke

但是,在goroutine 2等待运行时,goroutine 1将再次请求锁定:

7f9a5e6d931c2d9a71b07b70109a2d94.png

Figure 3— goroutine 2 is waiting to run

当goroutine 2尝试获取锁时,它将看到它已经具有保持状态并进入等待模式,如图2所示:

e63a9d6890ec8b404cab7da660ec7d47.png

Figure 4— goroutine 2 tries again to get the lock

goroutine 2对锁的获取将取决于它在线程上运行所花费的时间。

现在已经确定了问题,让我们回顾可能的解决方案。

Barging vs Handoff vs Spinning

处理互斥量的方法有很多,例如:

  • Barging旨在提高吞吐量。释放锁后,它将唤醒第一个服务员,并将锁提供给第一个传入请求或此唤醒的服务员:
8392d5ec5828d157de1769dc277545c4.png

barging mode

Go 1.8就是这样设计的,它反映了我们之前看到的内容。

  • Handoff释放后,互斥体将持有该锁,直到第一个服务员准备好获取它为止。这会降低吞吐量,因为即使另一个goroutine准备获取它,该锁也被保留了:
f88097b186028a0953a6e001a5fb2e4f.png

handoff mode

我们可以在Linux内核的互斥体中找到此逻辑:

发生锁饥饿是可能的,因为Mutex_lock()允许锁窃取,其中正在运行(或乐观旋转)的任务击败了唤醒的服务生进行获取。

锁窃取是一项重要的性能优化,因为等待服务员醒来并获取运行时可能会花费大量时间,在此期间,每个男孩都将锁在锁上。

[…]这重新引入了一些等待时间,因为一旦我们进行了切换,我们就必须等待服务员再次醒来。

在我们的情况下,互斥锁切换会完美平衡两个goroutine之间的锁分配,但是会降低性能,因为这将迫使第一个goroutine即使未持有也要等待锁。

  • Spinning如果互斥锁与自旋锁不同,则它可以结合一些逻辑。当服务员的队列为空或应用程序大量使用互斥锁时,旋转很有用。Parking和unparking goroutine的成本较高,可能比仅旋转等待下一个锁获取要慢:
93aa897a9035289f01baa942e382b592.png

spinning mode

Go 1.8也使用此策略。当试图获取已经持有的锁时,如果本地队列为空且处理器数量大于一,则goroutine将旋转几次-如果仅使用一个处理器旋转就会阻塞程序。旋转后,goroutine将停放。如果程序大量使用锁,它可以作为快速路径。

有关如何设计锁的更多信息(插入,越区切换,自旋锁),通常,Filip Pizlo撰写了必读的文章“ WebKit中的锁定 ”。

饥饿模式

在Go 1.9之前,Go结合了插入和旋转模式。在1.9版中,Go通过添加新的饥饿模式解决了先前解释的问题,该模式将导致在解锁模式期间进行切换。

所有等待锁定时间超过一毫秒的goroutine,也称为有界等待,将被标记为饥饿。当标记为饥饿时,解锁方法现在将把锁直接移交给第一位服务员。这是工作流程:

96392797d173d63586e3ef67c524a5f1.png

starvation mode

由于进入的goroutine将不会获取任何为下一个服务员保留的锁,因此在饥饿模式下也将禁用旋转。

让我们使用Go 1.9和新的starvation模式运行前面的示例:

Lock acquired per goroutine:g1: 57g2: 10

现在的结果更加公平。现在,我们想知道新的控制层是否会对互斥体不处于饥饿状态的其他情况产生影响。正如我们在该程序包的基准测试(Go 1.8与Go 1.9)中所看到的,在其他情况下,性能并没有下降(不同处理器数量下,性能会略有变化):

Cond32-6 10.9µs ± 2% 10.9µs ± 2% ~MutexUncontended-6 2.97ns ± 0% 2.97ns ± 0% ~Mutex-6 122ns ± 6% 122ns ± 2% ~MutexSlack-6 149ns ± 3% 142ns ± 3% -4.63%MutexWork-6 136ns ± 3% 140ns ± 5% ~MutexWorkSlack-6 152ns ± 0% 138ns ± 2% -9.21%MutexNoSpin-6 150ns ± 1% 152ns ± 0% +1.50%MutexSpin-6 726ns ± 0% 730ns ± 1% ~RWMutexWrite100-6 40.6ns ± 1% 40.9ns ± 1% +0.91%RWMutexWrite10-6 37.1ns ± 0% 37.0ns ± 1% ~RWMutexWorkWrite100-6 133ns ± 1% 134ns ± 1% +1.01%RWMutexWorkWrite10-6 152ns ± 0% 152ns ± 0% ~

翻译自:https://medium.com/a-journey-with-go/go-mutex-and-starvation-3f4f4e75ad50

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值