美文网首页
Go:互斥体和饥饿

Go:互斥体和饥饿

作者: 开心人开发世界 | 来源:发表于2019-10-15 06:32 被阅读0次

在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 <-done:
                return
            default:
                mu.Lock()
                time.Sleep(100 * time.Microsecond)
                mu.Unlock()
            }
        }
    }()

    // goroutine 2
    for i := 0; i < 10; i++ {
        time.Sleep(100 * time.Microsecond)
        mu.Lock()
        mu.Unlock()
    }
    done <- true
}

starvation.go

此示例基于两个goroutine:

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

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

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

Lock acquired per goroutine:
g1: 7200216
g2: 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 vs Handoff vs Spinning

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

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

barging mode

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

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

handoff mode

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

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

锁窃取是一项重要的性能优化,因为等待服务员醒来并获取运行时可能会花费大量时间,在此期间,每个男孩都将锁在锁上。
[…]这重新引入了一些等待时间,因为一旦我们进行了切换,我们就必须等待服务员再次醒来。

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

  • Spinning如果互斥锁与自旋锁不同,则它可以结合一些逻辑。当服务员的队列为空或应用程序大量使用互斥锁时,旋转很有用。Parking和unparking 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模式运行前面的示例:

Lock acquired per goroutine:
g1: 57
g2: 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

相关文章

  • Go:互斥体和饥饿

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

  • 信号量、互斥体和自旋锁

    互斥体 互斥体实现了“互相排斥”(mutual exclusion)同步的简单形式(所以名为互斥体(mute...

  • Go 语言的锁

    Go 语言提供两类锁: 互斥锁(Mutex)和读写锁(RWMutex)。其中读写锁(RWMutex)是基于互斥锁(...

  • golang-互斥锁 sync.Mutex

    互斥量(锁) 在go语言里面很少用,但是也提供支持 ! 互斥量的概念!go语言提供的锁:Mutex (sync.M...

  • C 语言的 互斥锁、自旋锁、原子操作

    今天不整 GO 语言,我们来分享一下以前写的 C 代码,来看看 互斥锁,自旋锁和原子操作的 demo 互斥锁 临界...

  • golang 基础(31) 锁

    在 go 语言中提供了并发编程模型和工具外,还提供传统的同步工具锁,包括互斥锁和读写锁有关互斥锁有些内容我们必须清...

  • Go超时锁的设计和实现

    Go提供两种锁:sync.Mutex和sync.RWMutex。 sync.Mutex: 互斥锁。任意时刻,只能有...

  • 第04天(面对对象编程)_02

    05_结构体指针类型匿名字段.go 06_面向过程和对象函数的区别.go 07_为结构体类型添加方法.go 08_...

  • Golang 读写锁设计

    在《Go精妙的互斥锁设计》一文中,我们详细地讲解了互斥锁的实现原理。互斥锁为了避免竞争条件,它只允许一个线程进入代...

  • (七)golang 互斥量 源码分析

    互斥锁 源码位置:https://github.com/golang/go/blob/master/src/syn...

网友评论

      本文标题:Go:互斥体和饥饿

      本文链接:https://www.haomeiwen.com/subject/mfakmctx.html