Go 语言并发编程之互斥锁详解 sync.Mutex

01 

介绍

Go 标准库 sync 提供互斥锁 Mutex。它的零值是未锁定的 Mutex,即未被任何 goroutine 所持有,它在被首次使用后,不可以复制。

我们可以使用 Mutex 限定同一时间只允许一个 goroutine 访问和修改临界区。

02 

使用

在介绍怎么使用 Mutex 之前,我们先阅读 `sync.Mutex` 源码[1]

// [the Go memory model]: https://go.dev/ref/mem
type Mutex struct {
 state int32
 sema  uint32
}
func (m *Mutex) Lock() {
// ...
}
func (m *Mutex) TryLock() bool {
// ...
}
func (m *Mutex) Unlock() {
// ...
}

 

阅读源码,我们可以发现,Mutex 提供了三个方法,分别是 LockUnlock 和 Go 1.18 新增的 TryLock

我们可以使用 MutexLock 方法获取锁,获取锁的 goroutine 可以访问和修改临界区,此时其它 goroutine 如果也想要访问和修改临界区,则会被阻塞,等待当前获取锁的 goroutine 释放锁。

持有锁的 goroutine 释放锁,可以使用  MutexUnlock 方法。

细心的读者朋友们,可能已经发现,Go 1.18 新增的 TryLock 是三个方法中唯一有返回值的方法,因为 TryLock 方法可以通过 bool 返回值通知当前准备争抢锁的 goroutine 是否抢到锁,该 goroutine 可以根据返回值决定做什么,而不仅是被阻塞,还可以自由选择做其它事情。

推荐读者朋友们阅读 the Go memory model[2],更加深入了解 Mutex

使用方式

单变量

func main() {
    // 定义变量 mu
    var mu sync.Mutex
    
    go func() {
        mu.Lock()
        fmt.Println("g1 get lock")
        time.Sleep(time.Second * 10)
        mu.Unlock()
    }()
    
    time.Sleep(time.Second * 5)
    
    // main goroutine
    if mu.TryLock() {
        fmt.Println("main get lock")
        mu.Unlock()
    } else {
        // main goroutine not get lock, do other thing
        fmt.Println("do other things first")
    }
}

  

struct 字段

type Counter struct {
    mu sync.Mutex
    count map[string]int
}

func main() {
    c := &Counter{
        count: make(map[string]int),
    }
    
    go func() {
        c.mu.Lock()
        c.count["lucy"] = 1
        fmt.Println("g1 goroutine:",c.count)
        time.Sleep(time.Second * 5)
        c.mu.Unlock()
    }()
    
    time.Sleep(time.Second * 2)
    
    // main goroutine
    c.count["lucy"] = 10
    fmt.Println("main goroutine:",c.count)
}

  

阅读代码,细心的读者朋友们可能已经发现,不管是单变量,还是作为 struct 中的字段,我们都未初始化 sync.Mutex 类型的变量,而是直接使用它的零值。

当然,初始化也可以。

03 

陷阱

想要用好 Mutex,我们还需要注意一些“陷阱”。

陷阱一

Go 语言中的互斥锁 Mutex,即使一个 goroutine 未持有锁,它也可以执行 Unlock 释放锁。

如果一个 goroutine 先使用 Unlock 释放锁,则会触发 panic。不管被释放的锁是一个未被任何 goroutine 持有的锁,还是正在被其它 goroutine 持有中的锁。

所以,我们在使用互斥锁 Mutex 时,遵循 “谁持有,谁释放” 原则。

陷阱二

假如我们在使用 Mutex 时,只使用 Lock 持有锁,而忘记使用 Unlock 释放锁,则会导致被阻塞中的 goroutine 一直被阻塞。

所以,我们在使用 Lock 时,可以在 mu.Lock() 后面,紧接着写一行 defer mu.Unlock(),当然,也要根据实际情况,灵活使用释放锁的方式,不一定必须使用 defer 的方式。

陷阱三

互斥锁 Mutex 在被首次使用后,不可以复制。

func main() {
    var mu sync.Mutex
    var mu2 sync.Mutex
    
    go func(){
        mu.Lock()
        defer mu.Unlock()
        fmt.Println("g1 goroutine")
        time.Sleep(time.Second * 10)
    }()
    
    time.Sleep(time.Second * 5)
    
    mu2 = mu
    
    mu2.Lock()
    fmt.Println("main goroutine")
    mu2.Unlock()
}

  

 

阅读代码,mu2 复制 mu 的值,程序会报错,因为 mu 已经被 goroutine 调用,它的底层值已经发生变化,所以 mu2 得到的不是一个零值的 Mutex

不过该错误可以被 go vet 检查到。

04 

延伸

我们在文中使用的代码,可以很容易知道临界区。但是,在实际项目中,我们会有一些复杂代码,即不太容易知道临界区的代码。

此时,我们可以使用数据竞争检测器,即 -race,需要注意的是,它是在运行时进行数据竞争检测,并且它比较耗费内存,在生产环境中不要使用。

使用方式:

go run -race main.go

05 

总结

本文我们介绍 Go 并发编程中,经常会使用的 sync 标准库中的互斥锁 Mutex

文中的示例代码,未给出输出结果,意在希望读者朋友们可以亲自动手执行代码,这样可以帮助大家理解文章内容。

posted @ 2024-09-29 08:36  林台山人  阅读(7)  评论(0编辑  收藏  举报