Go并发之Mutex
date
Feb 22, 2023
slug
go-mutex
author
status
Public
tags
Blog
Golang
summary
type
Post
thumbnail
category
updatedAt
Mar 15, 2023 09:46 AM
Mutex
Go语言中提供了标准库sync.mutex用来解决高并发中资源访问的问题,本文参考极客时间专栏《Go并发编程实战课》
实现机制
说白了就是一个资源在被某个goroutine持有的时候,其他goroutine不能进入临界区,否则就会返回失败,或者等待。那什么是临界区呢?
在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。
使用方法
Mutex实际上就是实现了package sync里的Locker接口
type Locker interface {
Lock()
Unlock()
}
可以看到这个接口简单粗暴,就是加锁和解锁,很符合Golang的风格··
Mutex的使用实际上也就是这么简单,进入临界区Lock,退出临界区的时候Unlock
func(m *Mutex)Lock()
func(m *Mutex)Unlock()
理解锁的作用之前,先来看看反面教材,当我们用10个goroutine分别给某个变量加10000(一万),正常情况下预期的结果是这个变量最后的值是100000(十万),不加锁的情况下:
package main
import (
"fmt"
"sync"
)
func main() {
count := 0
wg := sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 10000; j++ {
count++
}
}()
}
wg.Wait()
fmt.Println(count)
}
运行结果不符合预期,因为count++ 不是原子操作,我的理解是虽然每次加的操作都成功了,但是当前的操作只会关注当前goroutine操作前读取到的值,哪怕有其他goruntine同时执行➕操作,当前操作也是感知不到的,这就造成得到的值永远比预期的要小。
这个案例是我们故意写出来的,所以错误显而易见。在一个大型的程序中,我们需要排查这种问题的工具:Go race detector,它是基于google的C/C++ sanitizers技术实现的,编译器通过探测所有内存访问,加入代码监视这些内存地址的访问(读/写),在代码运行的时候,能够监视到变量的非同步访问
go race detector使用方法
go run -race mutex.go
信息很详细,可以准确的定位到data race的问题(生产环境慎用,根据原理可以看出比较影响性能)
这里有一点需要注意:Mutex 的零值是还没有 goroutine 等待的未加锁的状态,所以你不需要额外的初始化,直接声明变量(如 var mu sync.Mutex)即可。
mutex正是用来解决这种问题,使用方法如下,进入临界区lock,离开时unlock·
····
for j := 0; j < 10000; j++ {
mu.Lock()
count++
mu.Unlock()
}
····
问题解决
~ go run -race mutex.go
100000
如何优雅的使用mutex
在一些开源的项目中,我们会看到sync.Mutex会嵌入到struct中使用
type Counter struct{
mu sync.Mutex // 不用初始化,一般放在控制字段的上面
Count uint64
other string
}
通过嵌入字段,可以在struct上直接调用Lock/Unlock方法
func main() {
var counter Counter
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100000; j++ {
counter.Lock()
counter.Count++
counter.Unlock()
}
}()
}
wg.Wait()
fmt.Println(counter.Count)
}
type Counter struct {
sync.Mutex
Count uint64
}
拓展
果 Mutex 已经被一个 goroutine 获取了锁,其它等待中的 goroutine 们只能一直等待。那么,等这个锁释放后,等待中的 goroutine 中哪一个会优先获取 Mutex 呢?
等待的goroutine们是以FIFO排队的
- 当mutex处于正常状态的时候,若此时没有新的goroutine与队头goroutine竞争,则队头goroutine获得,如有,则新的goroutine大概率获得
- 当队头goroutine竞争锁失败1ms后,它会将Mutex调整为饥饿模式。进入饥饿模式后,锁的所有权会直接从解锁goroutine移交给队头goroutine,此时新来的goroutine直接放入队尾。
- 当一个goroutine获取锁后,如果发现自己满足下列条件中的任何一个#1它是队列中最后一个#2它等待锁的时间少于1ms,则将锁切换回正常模式