Summary
Go claims to be born for high concurrency. In high concurrency scenarios, it is bound to involve competition for public resources. When the corresponding scene occurs, we often use the Lock() and Unlock() methods of mutex to occupy or release resources. Although the call is simple, the internals of mutex involve a lot. Today, let us study it carefully.
Preliminary understanding of mutex
The source code of mutex is mainly in the src/sync/mutex.go
file, and its structure is relatively simple, as follows:
type Mutex struct {
state int32
sema uint32
}
We can see that there is a field sema, which represents the semaphore flag bit. The so-called semaphore is used to block or wake up between Goroutines. This is a bit like the PV primitive operation in the operating system. Let’s first understand the PV primitive operation:
PV primitive explanation:
The problem of synchronization and mutual exclusion between processes is handled by operating the semaphore S.
S>0: indicates that there are S resources available; S=0 indicates that no resources are available; the absolute value of S<0 indicates the number of processes in the waiting queue or linked list. The initial value of the semaphore S should be greater than or equal to zero.
P primitive: Means to apply for a resource, and subtract 1 atomically from S. If S>=0 after subtracting 1, the process will continue to execute; if S<0 after subtracting 1, it means that no resources are available and you need to change yourself Block it up and put it on the waiting queue.
V primitive: It means to release a resource and add 1 to S atomically; if 1 is added and S>0, the process continues to execute; if 1 is added, S<=0, it means there is a waiting process on the waiting queue, and it needs to be The first waiting process wakes up.
Through the above explanation, mutex can use the semaphore to block and arouse the goroutine.
In fact, the mutex is essentially about semaphore of blocking evoke operation.
When the goroutine cannot occupy the lock resource, it will be blocked and suspended, and cannot continue to execute the following code logic.
When mutex releases the lock resource, it will continue to evoke the previous goroutine to grab the lock resource.
As for the state field of mutex, it is used for state circulation. These state values involve some concepts. Let's explain in detail below.
mutex status flag
The mutex's state has 32 bits, and its lower 3 bits represent 3 states: wake state , lock state , starvation state , and the remaining digits indicate the number of goroutines currently blocked and waiting.
normal mode , starvation mode or spin according to the current state state.
mutex normal mode
When mutex calls the Unlock() method to release the lock resource, if there is a Goroutine queue waiting to be called up, the Goroutine at the head of the queue will be called up.
After the goroutine at the head of the team is called up, it will call the CAS method to modify the state state tentatively. If the modification is successful, it means that the lock resource is successfully occupied.
(Note: CAS is implemented in Go using atomic.CompareAndSwapInt32(addr *int32, old, new int32) method. CAS is similar to optimistic locking. Before modification, it will first determine whether the address value is still the old value, and only if it is the old value Continue to modify to the new value, otherwise it will return false to indicate that the modification failed.)
mutex starvation mode
Since the above Goroutine does not directly occupy resources after being awakened, it is also necessary to call the CAS method to to try occupy lock resources. If there is a new Goroutine at this time, it will also call the CAS method to try to occupy resources.
But for Go's scheduling mechanism, it will be more inclined to run the Goroutine with a shorter CPU time, and this will cause a certain probability for the new Goroutine to acquire the lock resource. At this time, the Goroutine at the head of the team will always occupy Less than that, resulting in starving to death .
In response to this situation, Go uses a starvation mode. That is, by judging that the head of the team Goroutine is still unable to obtain resources after a certain period of time, when Unlock releases the lock resource, the lock resource will be directly handed over to the head of the team Goroutine, and the current state is changed to starvation mode .
Later, if a new Goroutine is found to be in starvation mode, it will be directly added to the end of the waiting queue.
mutex spin
If Goroutine holding the lock resource relatively short time, so every call blocking semaphore to evoke goroutine, it will be very waste resources.
Therefore, after meeting certain conditions, mutex will let the current Goroutine go to idle the CPU, and call the CAS method again to try to occupy the lock resource after the idle run, until the spin condition is not met, it will eventually be added to the waiting queue .
The conditions for spin are as follows:
- Haven't spin more than 4 times
- Multi-core processor
- GOMAXPROCS > 1
- The local Goroutine queue on p is empty
It can be seen that the spin condition is still relatively strict, after all, this will consume the computing power of the CPU.
Lock() process of mutex
First, if the state of the mutex = 0, no one is occupying the resource, nor is it blocking the goroutine waiting to be called. The CAS method will be called to try to hold the lock, and no other actions will be taken.
If m.state = 0 is not met, it is further judged whether spin is needed.
When spinning is not needed or resources are still not available after spinning, the runtime_SemacquireMutex semaphore function is called at this time to block the current goroutine and add it to the waiting queue.
When the lock resource is released and the mutex awakens the goroutine at the head of the team, the goroutine at the head of the team will try to occupy the lock resource, and at this time, it may also compete with the newly arrived goroutine.
When the head goroutine has not been able to obtain resources, it will enter the starvation mode and directly hand the lock resource to the head goroutine, so that the new goroutine will block and join the end of the waiting queue.
The starvation mode will continue until the goroutine queue waiting to be called up is not blocked before it will be released.
Unlock process
Unlock() of mutex is relatively simple. Similarly, a quick unlock will be performed first, that is, there is no goroutine waiting to be awakened, and there is no need to continue to do other actions.
If it is currently in normal mode, simply evokes team Goroutine. If starvation mode, it will directly the lock to the team head Goroutine, then arouse team head Goroutine, it continues to run.
Detailed explanation of mutex code
Okay, the above general process is over, the detailed code process will be presented below, so that everyone can know the Lock() and Unlock() method logic of mutex in more detail.
Detailed explanation of mutex Lock() code:
// Lock mutex 的锁方法。
func (m *Mutex) Lock() {
// 快速上锁.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// 快速上锁失败,将进行操作较多的上锁动作。
m.lockSlow()
}
func (m *Mutex) lockSlow() {
var waitStartTime int64 // 记录当前 goroutine 的等待时间
starving := false // 是否饥饿
awoke := false // 是否被唤醒
iter := 0 // 自旋次数
old := m.state // 当前 mutex 的状态
for {
// 当前 mutex 的状态已上锁,并且非饥饿模式,并且符合自旋条件
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 当前还没设置过唤醒标识
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
runtime_doSpin()
iter++
old = m.state
continue
}
new := old
// 如果不是饥饿状态,则尝试上锁
// 如果是饥饿状态,则不会上锁,因为当前的 goroutine 将会被阻塞并添加到等待唤起队列的队尾
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 等待队列数量 + 1
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// 如果 goroutine 之前是饥饿模式,则此次也设置为饥饿模式
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
//
if awoke {
// 如果状态不符合预期,则报错
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// 新状态值需要清除唤醒标识,因为当前 goroutine 将会上锁或者再次 sleep
new &^= mutexWoken
}
// CAS 尝试性修改状态,修改成功则表示获取到锁资源
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 非饥饿模式,并且未获取过锁,则说明此次的获取锁是 ok 的,直接 return
if old&(mutexLocked|mutexStarving) == 0 {
break
}
// 根据等待时间计算 queueLifo
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
// 到这里,表示未能上锁成功
// queueLife = true, 将会把 goroutine 放到等待队列队头
// queueLife = false, 将会把 goroutine 放到等待队列队尾
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 计算是否符合饥饿模式,即等待时间是否超过一定的时间
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
old = m.state
// 上一次是饥饿模式
if old&mutexStarving != 0 {
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 此次不是饥饿模式又或者下次没有要唤起等待队列的 goroutine 了
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)
break
}
// 此处已不再是饥饿模式了,清除自旋次数,重新到 for 循环竞争锁。
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
Detailed explanation of mutex Unlock() code:
// Unlock 对 mutex 解锁.
// 如果没有上过锁,缺调用此方法解锁,将会抛出运行时错误。
// 它将允许在不同的 Goroutine 上进行上锁解锁
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 快速尝试解锁
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// 快速解锁失败,将进行操作较多的解锁动作。
m.unlockSlow(new)
}
}
func (m *Mutex) unlockSlow(new int32) {
// 非上锁状态,直接抛出异常
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// 正常模式
if new&mutexStarving == 0 {
old := new
for {
// 没有需要唤起的等待队列
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 唤起等待队列并数量-1
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false, 1)
return
}
old = m.state
}
} else {
//饥饿模式,将锁直接给等待队列的队头 goroutine
runtime_Semrelease(&m.sema, true, 1)
}
}
Interested friends can search the public account "Read New Technology" to follow more push articles.
you can, please like, leave a comment, share, thank you for your support!
Read new technology, read more new knowledge.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。