Go语言并发编程中Mutex 正常模式和饥饿模式

发布于 1周前 作者 itying888 最后一次编辑是 5天前 来自 问答

Go语言并发编程中Mutex 正常模式和饥饿模式

Go语言正常模式(非公平锁)

正常模式下,所有等待锁的 goroutine 按照 FIFO(先进先出)顺序等待。唤醒 的 goroutine 不会直接拥有锁,而是会和新请求 goroutine 竞争锁。新请求的 goroutine 更容易抢占:因为它正在 CPU 上执行,所以刚刚唤醒的 goroutine有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的 goroutine 会加入 到等待队列的前面。

Go语言饥饿模式(公平锁)

为了解决了等待 goroutine 队列的长尾问题饥饿模式下,直接由 unlock 把锁交给等待队列中排在第一位的 goroutine (队 头),同时,饥饿模式下,新进来的 goroutine 不会参与抢锁也不会进入自旋状 态,会直接进入等待队列的尾部。这样很好的解决了老的 goroutine 一直抢不 到锁的场景。 饥饿模式的触发条件:当一个 goroutine 等待锁时间超过 1 毫秒时,或者当前 队列只剩下一个 goroutine 的时候,Mutex 切换到饥饿模式。

总结 对于两种模式,正常模式下的性能是最好的,goroutine 可以连续多次获取锁,饥饿模式解决了取锁公平的问题,但是性能会下降,这其实是性能和公平 的一个平衡模式。

1 回复

在Go语言的并发编程中,sync.Mutex 是用来保护共享资源以避免竞态条件的基本工具。它提供了互斥锁定机制,确保同一时间只有一个goroutine能访问受保护的资源。sync.Mutex 本身并没有直接提供所谓的“饥饿模式”,但我们可以通过不同的使用方式或者结合其他同步机制来模拟或避免饥饿现象。

正常模式

在“正常模式”下,sync.Mutex 的行为遵循FIFO(先进先出)原则,即哪个goroutine先请求锁,哪个goroutine就先获得锁。这通常是期望的行为,因为它避免了某些goroutine因为其他goroutine持续请求锁而被“饿死”的情况。

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, mu *sync.Mutex, wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    fmt.Printf("Worker %d is working\n", id)
    time.Sleep(1 * time.Second) // 模拟耗时操作
    mu.Unlock()
    fmt.Printf("Worker %d finished\n", id)
}

func main() {
    var mu sync.Mutex
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(i, &mu, &wg)
    }

    wg.Wait()
}

避免饥饿

虽然sync.Mutex没有直接的“饥饿模式”,但你可以通过设计避免饥饿的发生,或者考虑使用其他同步机制,如sync.RWMutex(读写互斥锁)或者channel来减少锁的竞争。

然而,如果你想要模拟或解决特定情况下可能出现的饥饿问题,可以考虑使用更复杂的锁机制,如带优先级的锁或公平锁(Go标准库中没有直接提供,但可以通过其他方式实现)。

模拟解决饥饿(非标准库)

由于Go标准库中没有直接的“饥饿模式”支持,你可以通过维护一个goroutine队列来尝试公平地分配锁。但这种方法比较复杂,通常不推荐除非在极端情况下。

一个简化的思路是使用条件变量(在Go中通常通过sync.Cond实现)来手动控制goroutine的唤醒顺序,但这已经超出了sync.Mutex的范围,并且需要仔细设计以避免引入新的并发问题。

结论

在Go中,sync.Mutex默认行为即为“正常模式”,遵循FIFO原则。如果你遇到因锁竞争导致的性能问题或饥饿现象,考虑优化你的锁使用方式,如减少锁的持有时间,使用更细粒度的锁,或者改用其他同步机制如读写锁、通道等。在极端情况下,你可能需要设计更复杂的锁策略来避免饥饿,但这通常不推荐且难以维护。

回到顶部