摘要
并发是golang的一个重要能力。mutex是golang sync库中一个重要并发组件。其Lock()和Unlock()方法在并发中实践性极强。其内部Lock和Unlock方法实现较为复杂,本文尝试用流程图方式对其过程进行分析,并对其使用举一个小栗子。
原理分析
相关源码在sync/mutex.go中
数据结构
type Mutex struct {
state int32
sema uint32
}
其实现了Locker接口
type Locker interface {
Lock()
Unlock()
}
状态(state)
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
其含义如图所示
| 0 | 1 | 2 | 3 ~ |
|---|---|---|---|
| locked | woken | starving | waiting go routine count |
方法(成员函数)
Lock
graph TD
Start --> CAS加锁尝试;
CAS加锁尝试 --> B(try result)
B -- success -->End
B -- fail --> C[LockSlow]
C-->End
End
LockSlow
graph TD
Start --> A[加锁尝试循环]
A --> B[锁是否处于饥饿态]
B --yes--> C[canSpin目前自旋次数 cpu能否自旋]
C --不能--> H[锁如果不处于饥饿态, 加锁]
C --能--> D[该协程和锁均为未唤醒状态且锁等待线程数大于0]
D --no--> G
D --yes--> F[协程与锁状态均置为唤醒]
F --> G[doSpin cpu自旋 iter自旋次数+1]
G --> B
H --> I[如果锁处于饥饿态且有人占用锁, 等待协程数+1]
I --> J[如果协程是饥饿态且有人占用锁, 锁置为饥饿态]
J --> M[如果协程和锁处于唤醒态, 重置锁为未唤醒]
M --> N[CAS方式更新锁状态]
N --> O[锁被释放且不处于饥饿态]
O -- yes -->End
O -- no -->P[加入等待队列SemacquireMutex]
P -->如果协程等待时间超过1ms,协程变为饥饿模式-->Q[当前状态是否为饥饿]
Q -- yes -->R[等待协程数-1并加锁, 如果等待协程数减到0接触饥饿状态]
R --> 更新状态 --> 重置次数iter-->A
End
Unlock
graph TD
Start --> 解锁;
解锁 --> 解锁后状态
解锁后状态 --> B(== 0)
B -- yes -->End
B -- no<br/>存在等待解锁goroutine<br/> --> C[UnLockSlow]
C-->End
End
UnlockSlow
graph TD
Start --> 锁当前状态;
锁当前状态 --> B(是否处于饥饿态)
B -- yes -->D(是否存在其他协程等待加锁)
D -- yes -->E[唤醒锁并使等待协程数减1]
D -- no -->End
E --> F[通过semerelease唤醒一个协程]-->End
B -- no --> C[通过semerelease唤醒一个协程]
C-->End
End
使用
func doSth(mutex *sync.Mutex, do string) {
mutex.Lock()
defer mutex.Unlock()
fmt.Println("start" + do)
time.Sleep(time.Millisecond * 1000)
fmt.Println("end" + do)
}
func main() {
mutex := &sync.Mutex{}
for i := 0; i < 5; i++ {
doSth(mutex, strconv.Itoa(i))
}
for {
//不让主goroutine关闭
}
}
不加锁执行效果
加锁执行效果