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
提供了三个方法,分别是 Lock
、Unlock
和 Go 1.18 新增的 TryLock
。
我们可以使用 Mutex
的 Lock
方法获取锁,获取锁的 goroutine
可以访问和修改临界区,此时其它 goroutine
如果也想要访问和修改临界区,则会被阻塞,等待当前获取锁的 goroutine
释放锁。
持有锁的 goroutine
释放锁,可以使用 Mutex
的 Unlock
方法。
细心的读者朋友们,可能已经发现,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
。
文中的示例代码,未给出输出结果,意在希望读者朋友们可以亲自动手执行代码,这样可以帮助大家理解文章内容。
标签:goroutine,sync,mu,互斥,Unlock,Mutex,time,main From: https://www.cnblogs.com/dfsxh/p/18438798