Mutex 使用
Mutex 是使用最广泛的同步原语(Synchronization
primitives,有人也叫做并发原语。我们在这个课程中根据英文直译优先用同步原语,但是
并发原语的指代范围更大,还可以包括任务编排的类型,所以后面我们讲 Channel 或者扩
展类型时也会用并发原语)。关于同步原语,并没有一个严格的定义,你可以把它看作解
决并发问题的一个基础的数据结构。
go 当中 有
互斥锁 Mutex、读写锁 RWMutex、并发编排
WaitGroup、条件变量 Cond、Channel 等同步原语。
同步原语的适用场景。
**共享资源。**并发地读写共享资源,会出现数据竞争(data race)的问题,所以需要
Mutex、RWMutex 这样的并发原语来保护。
任务编排。需要 goroutine 按照一定的规律执行,而 goroutine 之间有相互等待或者依
赖的顺序关系,我们常常使用 WaitGroup 或者 Channel 来实现。
消息传递。信息交流以及不同的 goroutine 之间的线程安全的数据交流,常常使用
Channel 来实现。
Mutex 的基本使用方法
在正式看 Mutex 用法之前呢,我想先给你交代一件事:Locker 接口。
在 Go 的标准库中,package sync 提供了锁相关的一系列同步原语,这个 package 还定
义了一个 Locker 的接口,Mutex 就实现了这个接口。
Locker 的接口定义了锁同步原语的方法集:
1
2
3
4
|
type Locker interface {
Lock()
Unlock()
}
|
当一个 goroutine 通过调用 Lock 方法获得了这个锁的拥有权后, 其它请求锁的
goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放并且自己获取到了这个锁的拥
有权。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import (
"fmt"
"sync"
)
func main() {
var count = 0
// 使用WaitGroup等待10个goroutine完成
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 对变量count执行10次加1
for j := 0; j < 100000; j++ {
count++
}
}()
}
// 等待10个goroutine完成
wg.Wait()
fmt.Println(count)
}
|
在这段代码中,我们使用 sync.WaitGroup 来等待所有的 goroutine 执行完毕后,再输出
最终的结果。sync.WaitGroup 这个同步原语我会在后面具体介绍,现在你只需
要知道,我们使用它来控制等待一组 goroutine 全部做完任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import (
"fmt"
"sync"
)
func main() {
// 互斥锁保护计数器
var mu sync.Mutex
// 计数器的值
var count = 0
// 辅助变量,用来确认所有的goroutine都完成
var wg sync.WaitGroup
wg.Add(10)
// 启动10个gourontine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 累加10万次
for j := 0; j < 100000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}
|
多情况下,Mutex 会嵌入到其它 struct 中使用
1
2
3
4
|
type Counter struct {
mu sync.Mutex
Count uint64
}
|
采用嵌入字段的方式。通过嵌入字段,你可以在这个 struct 上直接调
用 Lock/Unlock 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
func (c *Counter) Incr() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// 得到计数器的值,也需要锁保护
func (c *Counter) Count() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
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)
}
|
使用 Mutex 实现一个线程安全的队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
type SliceQueue struct {
data []interface{}
mu sync.Mutex
}
func NewSliceQueue(n int) (q *SliceQueue) {
return &SliceQueue{data: make([]interface{}, 0, n)}
}
// Enqueue 把值放在队尾
func (q *SliceQueue) Enqueue(v interface{}) {
q.mu.Lock()
q.data = append(q.data, v)
q.mu.Unlock()
}
// Dequeue 移去队头并返回
func (q *SliceQueue) Dequeue() interface{} {
q.mu.Lock()
if len(q.data) == 0 {
q.mu.Unlock()
return nil
}
v := q.data[0]
q.data = q.data[1:]
q.mu.Unlock()
return v
}
|
其他
比如 Docker issue 37583、 35517、 32826、 30696等、kubernetes issue
72361、 71617等,都是后来发现的 data race 而采用互斥锁 Mutex 进行修复的。
总结
这里给一些不熟悉 go 需要的同学补充一下,go 语言查看汇编代码命令:
go tool compile -S file.go
对于文中 counter 的例子可以过度优化一下,那就是获取计数的 Count 函数其实可以通过读写锁,也就是 RWMutex 来优化一下。