Mutex
Mutex 互斥锁主要用来解决高并发的访问问题,常见的并发场景有:
- 多个 goroutine 并发更新同一个资源,像计数器;
- 同时更新用户的账户信息;
- 秒杀系统;往同一个 buffer 中并发写入数据等等。
如果没有互斥控制,就会出现一些异常情况,比如计数器的计数不准确、用户的账户可能出现透 支、秒杀系统出现超卖、buffer 中的数据混乱等等。
互斥锁的机制
互斥锁是并发控制的一个基本手段,是为了避免竞争而建立的一种并发控制机制。要了解互斥锁就先搞懂临界区的概念。
在并发编程中,如果程序中的一部分会被并发访问或修改,那么,为了避免并发访问导致的意想不到的结果,这部分程序需要被保护起来,这部分被保护起来的程序,就叫做临界区。
临界区就是一个被共享的资源,或者说是一个整体的一组共享资源,比如对数据库的访问、对某一个共享数据结构的操作、对一个 I/O 设备的使用、对一个连接池中的连接的调用,等等。
如果多个并发线程同时访问临界区,就会造成访问或者操作错误,此时就要用互斥锁,限定临界区只能同时由一个线程持有。
Mutex 基本用法
Mutex 实现了go的标准库 package sync
的Locker接口。
Locker 定义:
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
进入临界区(共享资源)之前调用Lock
方法,退出临界区的时候调用Unlock
方法。
当一个 goroutine 通过调用 Lock 方法获得了这个锁的拥有权后, 其它请求锁的 goroutine 就会阻塞在 Lock 方法的调用上,直到锁被释放并且自己获取到了这个锁的拥有权。
引用一个并发计数的示例来说明mutex的使用:
创建10 个 goroutine 不断多一个counter操作加1,期望计数的结果是 10 * 100000 = 1000000 (一百万)。
代码如下:
package main
import (
"fmt"
"sync"
)
func main() {
counter := 0
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 10000; j++ {
counter++
}
}()
}
wg.Wait()
fmt.Println(counter)
}
如果不加Mutex的时候,每次结果都不会是期望结果的 1000000
➜ mutex (master) ✗ go run counter.go
37727
➜ mutex (master) ✗ go run counter.go
37426
➜ mutex (master) ✗ go run counter.go
40452
这是因为count++
每次都不是原子操作并会有并发+1的情况。
go 官方提供了一个检测此类并发访问共享资源是否有问题的工具 data race
;
如果出现并发问题就会打印竞争警告信息,使用方法为go run --race counter.go
➜ mutex (master) ✗ go run --race counter.go
==================
WARNING: DATA RACE
Read at 0x00c00013c018 by goroutine 8:
main.main.func1()
/Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:16 +0x84
Previous write at 0x00c00013c018 by goroutine 7:
main.main.func1()
/Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:16 +0x98
Goroutine 8 (running) created at:
main.main()
/Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:13 +0xc4
Goroutine 7 (finished) created at:
main.main()
/Users/lsrong/Work/Project/Go/src/github.com/lsrong/gotraining/concurrency/mutex/counter.go:13 +0xc4
==================
50021
Found 1 data race(s)
exit status 66
除了运行时加入 --race
参数,运行结果最后显示Found 1 data race(s)
发现一个数据竞争问题(并发问题),除了在go run
运行是检测竞争问题,还有可以运行 go tool compile -race -S counter.go
, 查看编译后的代码,也可以发现竞争问题,如下:
......
0x0024 00036 (counter.go:8) PCDATA $1, ZR
0x0024 00036 (counter.go:8) CALL runtime.racefuncenter(SB)
0x0028 00040 (counter.go:9) MOVD $type.int(SB), R0
0x0030 00048 (counter.go:9) MOVD R0, 8(RSP)
0x0034 00052 (counter.go:9) CALL runtime.newobject(SB)
0x0038 00056 (counter.go:9) MOVD 16(RSP), R0
0x003c 00060 (counter.go:9) MOVD R0, "".&counter-24(SP)
0x0040 00064 (counter.go:9) MOVD R0, 8(RSP)
0x0044 00068 (counter.go:9) PCDATA $1, $1
0x0044 00068 (counter.go:9) CALL runtime.racewrite(SB)
0x0048 00072 (counter.go:9) MOVD "".&counter-24(SP), R0
0x004c 00076 (counter.go:9) MOVD ZR, (R0)
0x0050 00080 (counter.go:10) MOVD $type.sync.WaitGroup(SB), R1
0x0058 00088 (counter.go:10) MOVD R1, 8(RSP)
......
如果出现data race
Go 编译器会加上 runtime.racefuncenter(SB)
runtime.racewrite(SB)
等检测 data race的方法,这些指令用于 Go race detector工具能够检测出data race 的问题。
接下来就是就此类并发问题就可以使用Mutex
来处理,在进入临界区 count++
的时候获得锁,在离开临界区的时候释放锁.
代码如下:
package main
import (
"fmt"
"sync"
)
func main() {
var counter = 0
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(10)
for i := 0; i < 10; i++ {
// 并发 加1
go func() {
defer wg.Done()
for j := 0; j < 10000; j++ {
// 加锁
mu.Lock()
counter++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(counter)
}
此时运行结果就和预期的一致了,同时 data race 也没有警告信息。
➜ mutex (master) ✗ go run --race mutexcounter.go
100000
Mutex 嵌入到struct中
有时候还可以将Mutex
嵌入到自定义的stuct中,比如:
type Counter struct {
mu sync.Mutex
count uint64
}
这样初始化的时候就不用初始化Mutex
变量,还可以把获取锁、释放锁、计数加1的逻辑封装成一个方法,对外不暴露锁等逻辑。
代码如下:
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
count uint64
}
func (c *Counter) Incr() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
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)
// 开启 10个goroutine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 10万次累加
for j := 0; j < 100000; j++ {
counter.Incr() // 有锁的加持
}
}()
}
wg.Wait()
fmt.Println(counter.Count())
}
运行结果如下:
➜ mutex (master) ✗ go run --race mutexcounter_struct.go
1000000
总结
本文主要学习Mutex
的使用和实践,通过race detector工具发现并发问题,如果开发过程中也会遇到有类似并发的问题,都可以考虑用通过互斥锁的手段去解决。