Golang并发实践(1)sync.Mutex

159 阅读1分钟

摘要

并发是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

其含义如图所示

0123 ~
lockedwokenstarvingwaiting 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关闭
   }

}

不加锁执行效果

image.png

加锁执行效果

image.png