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状态枚举对应二进制对应状态
mutexUnLockstate=00000未加锁
mutexLockedstate=10001加锁
mutexWokenstate=20010唤醒
mutexStarvingstate=40100饥饿
mutexWaiterShiftstate=30011代表位移

在看下面代码之前,一定要记住这几个状态之间的 与运算 或运算,否则代码里的与运算或运算

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(分段锁)(尽量减小锁的颗粒度)

参考