8.2.1 sync.Mutex互斥锁
本书在第7章的案例中已经使用过互斥锁,那么,什么是互斥锁呢?互斥锁是Locker接口的一种具体实现,它定义了两个方法:
func (m *Mutex) Lock() func (m *Mutex) Unlock()
一个互斥锁只能被一个goroutine锁定,锁定的是这个锁本身,而不是某段代码。如果给某段代码加上锁,相当于执行这段代码的时候就加了锁,别的程序执行这段代码只能等待前面的程序结束并把锁打开,如此往复,以此来实现对变量的共享使用,下面来看一个简单的例子:
book/ch08/8.2/mutex/mutex.go
1. package main
2.
3. import (
4. "fmt"
5. "sync"
6. "time"
7. )
8.
9. var (
10. m sync.Mutex
11. v1 int
12. )
13.
14. func Set(i int) {
15. m.Lock()
16. time.Sleep(time.Second)
17. v1 = i
18. m.Unlock()
19. }
20. func Read() int {
21. m.Lock()
22. a := v1
23. m.Unlock()
24. return a
25.
26. }
27. func main() {
28.
29. numGR := 5
30.
31. var wg sync.WaitGroup
32. fmt.Printf("%d ", Read())
33. for i := 1; i <= numGR; i++ {
34. wg.Add(1)
35. go func(i int) {
36. defer wg.Done()
37. Set(i)
38. fmt.Printf("-> %d", Read())
39. }(i)
40. }
41.
42. wg.Wait()
43. }
第9行至第12行声明了两个变量,int型的v1和sync.Mutex型的m。要指出的是,在代码中一般把锁和变量放在一起定义,比如此处的m就是为了共享v1而使用的。如果在写代码的时候没有将锁和变量放到一起声明就一定要加注释,这是为了便于阅读代码。
第14行至第26行定义了两个方法:即Set和Read。可以看到,这两个方法里面都用了m.Lock和Unlock,可以自由操作加锁和开锁之间的代码,这时程序只有一个goroutine在运行此部分代码。也就是说,某一时刻最多只能有一个goroutine在执行Set和Read方法,此处的Set和Read用的是同一个m,如果有goroutine在执行Set,那么执行Read就会阻塞。
另外,请读者注意第22行和第23行,这里仅仅是读取一个v1的值,之所以不直接写作return v1,是因为要写m.Unlock方法。虽然在代码里看着是一行语句,但是在编译之后对应多行机器指令,这个return语句不是原子性的,这样写仍然可能出现在返回期间被修改的情况。现在的写法是先把值赋给a,然后解锁,再返回,而a仅在当前方法中使用。其实,更多时候应该使用defer。defer能保证程序报错也能正常解锁,而且代码更为清晰可读。上述代码可以改为如下形式:
m.Lock() defer m.Unlock() return v1
虽然代码中使用了defer后系统开销会变大,执行效率会降低,但是首先还是要保证程序的正确性,一定不要过早优化代码。上述示例中的代码非常简单,因此没有使用defer,实战当中还是要尽可能使用。
注意
sync.Mutex的加锁和开锁必须是成对的,如果加锁后忘记开锁,那么程序会崩溃。
sync.Mutex锁只能加锁一次,要想再次加锁则要等待开锁以后,不可理解为在几个地方使用就是可以并行的。互斥锁和要处理的变量是一对,凡是在操作变量的地方都应该加上互斥锁,就如本例中Set和Read方法,用的是同一个变量锁加锁。
这样做效率显然不高,因为读取变量也是串行的,那么有没有优化的方案呢?有,那就是使用多读写锁sync.RWMutex。