Go-Mutex互斥量
先來看一段go1.12.5中Mutex的源碼:
// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file.// Package sync provides basic synchronization primitives such as mutual // exclusion locks. Other than the Once and WaitGroup types, most are intended // for use by low-level library routines. Higher-level synchronization is // better done via channels and communication. // // Values containing the types defined in this package should not be copied. package syncimport ("internal/race""sync/atomic""unsafe" )func throw(string) // provided by runtime// A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct {state int32sema uint32 }// A Locker represents an object that can be locked and unlocked. type Locker interface {Lock()Unlock() }const (mutexLocked = 1 << iota // mutex is lockedmutexWokenmutexStarvingmutexWaiterShift = iota// Mutex fairness.//// Mutex can be in 2 modes of operations: normal and starvation.// In normal mode waiters are queued in FIFO order, but a woken up waiter// does not own the mutex and competes with new arriving goroutines over// the ownership. New arriving goroutines have an advantage -- they are// already running on CPU and there can be lots of them, so a woken up// waiter has good chances of losing. In such case it is queued at front// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,// it switches mutex to the starvation mode.//// In starvation mode ownership of the mutex is directly handed off from// the unlocking goroutine to the waiter at the front of the queue.// New arriving goroutines don't try to acquire the mutex even if it appears// to be unlocked, and don't try to spin. Instead they queue themselves at// the tail of the wait queue.//// If a waiter receives ownership of the mutex and sees that either// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,// it switches mutex back to normal operation mode.//// Normal mode has considerably better performance as a goroutine can acquire// a mutex several times in a row even if there are blocked waiters.// Starvation mode is important to prevent pathological cases of tail latency.starvationThresholdNs = 1e6 )// Lock locks m. // If the lock is already in use, the calling goroutine // blocks until the mutex is available. func (m *Mutex) Lock() {// Fast path: grab unlocked mutex.if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}var waitStartTime int64starving := falseawoke := falseiter := 0old := m.statefor {// Don't spin in starvation mode, ownership is handed off to waiters// so we won't be able to acquire the mutex anyway.if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// Active spinning makes sense.// Try to set mutexWoken flag to inform Unlock// to not wake other blocked goroutines.if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}runtime_doSpin()iter++old = m.statecontinue}new := old// Don't try to acquire starving mutex, new arriving goroutines must queue.if old&mutexStarving == 0 {new |= mutexLocked}if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// The current goroutine switches mutex to starvation mode.// But if the mutex is currently unlocked, don't do the switch.// Unlock expects that starving mutex has waiters, which will not// be true in this case.if starving && old&mutexLocked != 0 {new |= mutexStarving}if awoke {// The goroutine has been woken from sleep,// so we need to reset the flag in either case.if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}if atomic.CompareAndSwapInt32(&m.state, old, new) {if old&(mutexLocked|mutexStarving) == 0 {break // locked the mutex with CAS}// If we were already waiting before, queue at the front of the queue.queueLifo := waitStartTime != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}runtime_SemacquireMutex(&m.sema, queueLifo)starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.stateif old&mutexStarving != 0 {// If this goroutine was woken and mutex is in starvation mode,// ownership was handed off to us but mutex is in somewhat// inconsistent state: mutexLocked is not set and we are still// accounted as waiter. Fix that.if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}delta := int32(mutexLocked - 1<<mutexWaiterShift)if !starving || old>>mutexWaiterShift == 1 {// Exit starvation mode.// Critical to do it here and consider wait time.// Starvation mode is so inefficient, that two goroutines// can go lock-step infinitely once they switch mutex// to starvation mode.delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}}if race.Enabled {race.Acquire(unsafe.Pointer(m))} }// Unlock unlocks m. // It is a run-time error if m is not locked on entry to Unlock. // // A locked Mutex is not associated with a particular goroutine. // It is allowed for one goroutine to lock a Mutex and then // arrange for another goroutine to unlock it. func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}// Fast path: drop lock bit.new := atomic.AddInt32(&m.state, -mutexLocked)if (new+mutexLocked)&mutexLocked == 0 {throw("sync: unlock of unlocked mutex")}if new&mutexStarving == 0 {old := newfor {// If there are no waiters or a goroutine has already// been woken or grabbed the lock, no need to wake anyone.// In starvation mode ownership is directly handed off from unlocking// goroutine to the next waiter. We are not part of this chain,// since we did not observe mutexStarving when we unlocked the mutex above.// So get off the way.if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// Grab the right to wake someone.new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false)return}old = m.state}} else {// Starving mode: handoff mutex ownership to the next waiter.// Note: mutexLocked is not set, the waiter will set it after wakeup.// But mutex is still considered locked if mutexStarving is set,// so new coming goroutines won't acquire it.runtime_Semrelease(&m.sema, true)} } mutex.goMutex結構體
type Mutex struct {state int32 //互斥鎖的狀態sema uint32 //信號量,協程阻塞等待該信號量,解鎖的協程釋放信號量從而喚醒等待信號量的協程。 }查資料的時候找到一張很好的展示Mutex的內存布局的圖:
- Locked:表示該Mutex是否已被鎖定,0代表未被鎖定,1代表已被鎖定。
- Woken:表示是否有協程已被喚醒,0代表沒有協程被喚醒,1代表已有協程被喚醒,正在加鎖。
- Starving:表示該Mutex是否處于饑餓狀態,0代表不處于饑餓狀態,1代表處于饑餓狀態,即阻塞超過1ms。
- Waiter:表示阻塞等待鎖的協程個數,協程解鎖時根據此值來判斷是否需要釋放信號量。
協程之間搶鎖實際上是搶給Locked賦值的權利,能給Locked域置1,就說明搶鎖成功。搶不到就阻塞等待Mutex.sema信號量,一旦持有鎖的協程解鎖,等待的協程會依次被喚醒。
(這個作者寫的真好,原文鏈接我放到文末了~)
(有時候很恍惚,自己到底有沒有寫博客的必要,大部分時候都是互聯網的搬運工,總有一天,我也能寫出純原創的技術博客~)
兩種操作模式
go的互斥有兩種操作模式:正常模式和饑餓模式
在正常模式下,等待狀態的協程(等待者)按照FIFO順序排隊。一個由沉睡(sleep)醒轉(就緒)來的協程不擁有互斥鎖,并且它對于互斥鎖的競爭并不如新到來的協程有優勢。新到來的協程可能已經在CPU上運行,并且可能數量還很多。但是,如果醒轉來的等待者在隊列中阻塞等待互斥鎖超過1毫秒,正常模式將會切換到饑餓模式。
在饑餓模式下,互斥鎖的所有權直接從釋放鎖的協程傳遞到隊列最前的等待者。新到來的協程即使有競爭優勢,也不會去爭取互斥鎖,相反,它們會到隊列尾部排隊等候。
如果醒轉來的等待者獲得互斥鎖的所有權并且發現(1)它是隊列中的最后一個等待者,或者(2)它等待不到1ms,它便會把饑餓模式切換到正常模式。
正常模式具有相當好的性能,因為即使存在阻塞的等待者,協程也可以連續多次獲取互斥鎖。
而饑餓模式可以預防到達協程遲遲得不到處理(反而可能被遠遠排在新協程之后),說白了,饑餓模式就是為防止協程進入饑餓狀態忙等而設。
兩種方法
Mutex只提供了兩種方法,即Lock()加鎖和Unlock()解鎖。
加鎖
以上面那張Mutex內存布局圖為例,最簡單毫無阻塞的加鎖就是將Locked置為1,當鎖已被占用,則將Waiter++,協程進入阻塞,直到Locked值變為0后被喚醒。
解鎖
仍是以上面那張Mutex內存布局圖為例,沒有其他協程阻塞等待加鎖(即Waiter為0),則只要將Locked置為0,不需要釋放信號量。若解鎖時,有1個或多個協程阻塞(即Waiter>0),則需要在將Locked置為0后,釋放信號量喚醒阻塞的協程。
自旋
基本概念
上面我們說到,加鎖時可能被阻塞,此時,協程并不是立即進入阻塞,而是會持續檢測Locked是否變為0,這個過程即為自旋(spin)過程。從源碼mutex源碼中runtime_canSpin()和runtime_doSpin()兩個方法,它們就是用來判斷是否可以自旋(即是否符合自旋條件)和執行自旋的。
自旋必須滿足一下所有條件:
- 自旋次數要足夠小,通常為4,即每個協程最多自旋4次。
- CPU核數要大于1,否則自旋沒有意義,因為此時不可能有其他協程釋放鎖。
- 協程調度機制中的Process數量要大于1,比如使用GOMAXPROCS()將處理器設置為1就不能啟動自旋。
- 協程調度機制中的可運行隊列必須為空,否則會延遲協程調度。
限制自旋次數很好理解,關于CPU核數,一開始我不理解為什么要限制這個,不妨來設想一下協程自旋的場景,假設一個協程主動釋放了鎖并釋放了信號量,我們的協程在對處理器的競爭中慘敗,因此進入短暫自旋,以期尋找其他門路,即看看其他處理器是不是正有協程準備解鎖,試想,假如只有1核,剛剛在和我們的競爭中獲取取得鎖控制權的協程,怎么可能在短期內釋放鎖,因此只能直接進入阻塞。
至于什么是GOMAXPROCS呢?就是邏輯CPU數量,它可以被設置為如下幾種數值:
- <1:不修改任何數值。
- =1:單核心執行。
- >1:多核并發執行
一般情況下,可以使用runtime.NumCPU查詢CPU數量,并使用runtime.GOMAXPROCS()進行設置,如:runtime.GOMAXPROCS(runtime.NumCPU),將邏輯CPU數量設置為物理CPU數量。
現在想想,對Process進行限制,是不是顯而易見的事。
至于可運行隊列為什么必須為空,我的理解,就是當前只能有這一條就緒線程,也就是說同時只能有一條自旋。
自旋的好處
可以更充分的利用CPU。
自旋的壞處
如果協程通過自旋獲得鎖,那么之前被阻塞的協程將無法獲得鎖,如果加鎖的協程特別多,每次都通過自旋獲得鎖,那么之前被阻塞的協程將很難獲得鎖,從而進入饑餓狀態。因此,在1.8版本以后,饑餓狀態(即Starving為1)下不允許自旋。
補充
自旋和模式切換是有區別的,自旋發生在阻塞之前,模式切換發生在阻塞之后。
整個互斥過程是圍繞著Mutex量進行的,即爭奪對Mutex內存的修改權,Mutex可以看作是處理器的鑰匙,爭奪到Mutex的協程可以被處理。
Woken的作用:
Woken用于加鎖和解鎖過程的通信,譬如,同一時刻,有兩個協程,一個在加鎖,一個在解鎖,在加鎖的協程可能在自旋,此時把Woken置為1,通知解鎖協程不必釋放信號量了。也就是說Woken標志著當前有就緒狀態的進程,不用解鎖協程去通知。
參考鏈接
https://my.oschina.net/renhc/blog/2876211
另外還有一篇詳解mutex源碼的博客:
https://blog.csdn.net/qq_31967569/article/details/80987352
轉載于:https://www.cnblogs.com/StrayLesley/p/10943889.html
總結
以上是生活随笔為你收集整理的Go-Mutex互斥量的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 65寸电视长宽多少厘米详情(65寸电视长
- 下一篇: linux下怎样编辑文件保存文件?