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 来优化一下。