Go Mutex 的基本用法
Mutex
LockUnlock
Mutex
说明:
MutexG1->G2->GnUnlockLockUnlock
Mutex
Go Mutex 原子操作
Mutex
state
这四种不同信息在源码中定义了不同的常量:
sema
Mutexstate
但是毋庸置疑,这种实现会大大降低代码的可读性,因为通过一个整数来记录不同的信息, 就意味着,需要通过各种位运算来实现对这个整数不同位的修改。
Mutexstate
mutexLockedstate10
new := mutexLocked
mutexWoken
new = (old - 1<
mutexStarving
new |= mutexStarving
state >> mutexWaiterShiftFIFO
new += 1 << mutexWaiterShiftdelta := int32(mutexLocked - 1<
statenewCASMutexstate
Mutexstate
syncWaitGroupstate
CASCAS
CASstatestatestate
CASfor
state的状态及枚举
state状态 | state状态枚举 | 对应二进制 | 对应状态 |
---|---|---|---|
mutexUnLock | state=0 | 0000 | 未加锁 |
mutexLocked | state=1 | 0001 | 加锁 |
mutexWoken | state=2 | 0010 | 唤醒 |
mutexStarving | state=4 | 0100 | 饥饿 |
mutexWaiterShift | state=3 | 0011 | 代表位移 |
在看下面代码之前,一定要记住这几个状态之间的 与运算 或运算,否则代码里的与运算或运算
state: |32|31|...|3|2|1|
__________/ | |
| | |
| | mutex的占用状态(1被占用,0可用)
| |
| mutex的当前goroutine是否被唤醒
|
当前阻塞在mutex上的goroutine数
互斥锁的作用
互斥锁是保证同步的一种工具,主要体现在以下2个方面:
避免多个线程在同一时刻操作同一个数据块 (sum)
可以协调多个线程,以避免它们在同一时刻执行同一个代码块 (sum++)
什么时候用
需要保护一个数据或数据块时
需要协调多个协程串行执行同一代码块,避免并发问题时
比如 经常遇到A给B转账100元的例子,这个时候就可以用互斥锁来实现。
注意的坑
1. 不同 goroutine 可以 Unlock 同一个 Mutex,但是 Unlock 一个无锁状态的 Mutex 就会报错。
2. 因为 mutex 没有记录 goroutine_id,所以要避免在不同的协程中分别进行上锁/解锁操作,不然很容易造成死锁。
建议: 先 Lock 再 Unlock、两者成对出现。
3. Mutex 不是可重入锁
Mutex 不会记录持有锁的协程的信息,所以如果连续两次 Lock 操作,就直接死锁了。
如何实现可重入锁?记录上锁的 goroutine 的唯一标识,在重入上锁/解锁的时候只需要增减计数。
4.多高的 QPS 才能让 Mutex 产生强烈的锁竞争?
模拟一个 10ms 的接口,接口逻辑中使用全局共享的 Mutex,会发现在较低 QPS 的时候就开始产生激烈的锁竞争(打印锁等待时间和接口时间)。
解决方式:首先要尽量避免使用 Mutex。如果要使用 Mutex,尽量多声明一些 Mutex,采用取模分片的方式去使用其中一个 Mutex 进行资源控制。避免一个 Mutex 对应过多的并发。
简单总结:压测或者流量高的时候发现系统不正常,打开 pprof 发现 goroutine 指标在飙升,并且大量 Goroutine 都阻塞在 Mutex 的 Lock 上,这种现象下基本就可以确定是锁竞争。
5. Mutex 千万不能被复制
因为复制的时候会将原锁的 state 值也进行复制。复制之后,一个新 Mutex 可能莫名处于持有锁、唤醒或者饥饿状态,甚至等阻塞等待数量远远大于0。而原锁 Unlock 的时候,却不会影响复制锁。
关于锁的使用建议
写业务时不能全局使用同一个 Mutex
千万不要将要加锁和解锁分到两个以上 Goroutine 中进行(容易形成死锁)
Mutex 千万不能被复制(包括不能通过函数参数传递),否则会复制传参前锁的状态:已锁定 or 未锁定。很容易产生死锁,关键是编译器还发现不了这个 Deadlock~
尽量避免使用 Mutex,如果非使用不可,尽量多声明一些 Mutex,采用取模分片的方式去使用其中一个 Mutex(分段锁)(尽量减小锁的颗粒度)