go中的sync.Mutex

golang中的互斥鎖定義在src/sync/mutex.go

源碼中給出了互斥量公平的解釋,差不多意思如下:

互斥鎖可以處於兩種操作模式:normal和starvation。在normal模式下,新加入競爭鎖隊列的協程也會直接參與到鎖的競爭中來,處於starvation模式下,所以新加入的協程將直接進入等待隊列中掛起,直到其等待隊列之前的協程全部執行完畢。

normal模式下,協程的競爭等待時間如果大於1ms,就會進入starvation模式。starvation模式下,該協程是等待隊列中的最後一個工作協程,或者它掛起等待時長不到1ms,則切換回normal模式。

state用於存儲Mutex的狀態量,具體可以看下下面const,state的最低位(mutexLocked)用於表示是否上鎖,低二位(mutexWoken)用來表示當前鎖是否喚醒,低三位(mutexStarving)用來表示當前鎖是否處於starvation模式。剩下位數據state>>mutexWaitershif(mutexWaitershif爲3)用來表示當前被阻塞的協程數量,sema是一個信號量,協程阻塞的依據。

type Mutex struct {
	state int32
	sema  uint32
}
const (
	mutexLocked = 1 << iota // mutex is locked
	mutexWoken
	mutexStarving
	mutexWaiterShift = iota

	starvationThresholdNs = 1e6
)
我們來具體看下Lock()代碼
// 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 int64
	starving := false
	awoke := false
	iter := 0
	old := m.state
	for {
		// 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.state
			continue
		}
		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 != 0
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			runtime_SemacquireMutex(&m.sema, queueLifo)
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			if 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 = true
			iter = 0
		} else {
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
}

我們一點一點分析,一開始會通過cas嘗試將state的從0(0的時候即沒有協程獲得當前鎖)賦值成1,如果成功表示,當前這是當前鎖的第一次加鎖且成功,那直接返回。

if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		if race.Enabled {
			race.Acquire(unsafe.Pointer(m))
		}
		return
	}

如果加鎖失敗,那說明有協程已經獲得鎖,需等待鎖的釋放。於是第一個情況:當前處於normal模式且已經加鎖,於是去判斷是否可以自旋,如果可以自旋,再判斷是否還有協程處於阻塞狀態(在等待當前鎖),如果有,再通過cas將當前鎖狀態設置爲喚醒狀態,之後當前協程進行自旋。

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.state
			continue
		}

當自旋條件不符合,或者當前鎖被釋放或者當前鎖處於starvation模式,則進入循環的下一部分。

若此時由於別的協程佔用且無法獲得鎖,或者當前處於starvation模式,則給state的線程數加1即state加8;若當前處於starvating值爲true,且別的協程佔用鎖,則把當前鎖狀態置爲starvation模式;若之前自旋時將鎖喚醒,於是把低二位置爲0;之後通過cas將新的state賦值給state,如果失敗,那麼繼續重複之前的操作;如果成功,先判斷當前協程阻塞時間是否爲0,爲0則從現在開始計時,通過runtime_SemacquireMutex()方法阻塞當前協程;

我們來看下sema.go下的該方法

func sync_runtime_SemacquireMutex(addr *uint32, lifo bool) {
	semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile)
}

func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags) {
	gp := getg()
	if gp != gp.m.curg {
		throw("semacquire not on the G stack")
	}

	// Easy case.
	if cansemacquire(addr) {
		return
	}

	// Harder case:
	//	increment waiter count
	//	try cansemacquire one more time, return if succeeded
	//	enqueue itself as a waiter
	//	sleep
	//	(waiter descriptor is dequeued by signaler)
	s := acquireSudog()
	root := semroot(addr)
	t0 := int64(0)
	s.releasetime = 0
	s.acquiretime = 0
	s.ticket = 0
	if profile&semaBlockProfile != 0 && blockprofilerate > 0 {
		t0 = cputicks()
		s.releasetime = -1
	}
	if profile&semaMutexProfile != 0 && mutexprofilerate > 0 {
		if t0 == 0 {
			t0 = cputicks()
		}
		s.acquiretime = t0
	}
	for {
		lock(&root.lock)
		// Add ourselves to nwait to disable "easy case" in semrelease.
		atomic.Xadd(&root.nwait, 1)
		// Check cansemacquire to avoid missed wakeup.
		if cansemacquire(addr) {
			atomic.Xadd(&root.nwait, -1)
			unlock(&root.lock)
			break
		}
		// Any semrelease after the cansemacquire knows we're waiting
		// (we set nwait above), so go to sleep.
		root.queue(addr, s, lifo)
		goparkunlock(&root.lock, "semacquire", traceEvGoBlockSync, 4)
		if s.ticket != 0 || cansemacquire(addr) {
			break
		}
	}
	if s.releasetime > 0 {
		blockevent(s.releasetime-t0, 3)
	}
	releaseSudog(s)
}

此時傳入的是Mutex.sema的地址跟lifo(false),首先獲得gp指向當前協程,對信號量sema的值進行判斷,如果爲0,則繼續,否則嘗試減1並返回,通過semroot(addr)獲得semaRoot

func semroot(addr *uint32) *semaRoot {
	return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root
}

傳入的地址右移三位取餘251,得到semroot,做到semroot通過sema的地址關聯相應的Mutex;

type semaRoot struct {
	lock  mutex
	treap *sudog // root of balanced tree of unique waiters.
	nwait uint32 // Number of waiters. Read w/o the lock.
}

lock是與Mutex無關的一個通過uintptr實現的簡單的線程安全的功能,treap是平衡二叉樹的根,nwait記錄了平衡二叉樹中阻塞的協程的數量。

回到sema.go中

for {
		lock(&root.lock)
		// Add ourselves to nwait to disable "easy case" in semrelease.
		atomic.Xadd(&root.nwait, 1)
		// Check cansemacquire to avoid missed wakeup.
		if cansemacquire(addr) {
			atomic.Xadd(&root.nwait, -1)
			unlock(&root.lock)
			break
		}
		// Any semrelease after the cansemacquire knows we're waiting
		// (we set nwait above), so go to sleep.
		root.queue(addr, s, lifo)
		goparkunlock(&root.lock, "semacquire", traceEvGoBlockSync, 4)
		if s.ticket != 0 || cansemacquire(addr) {
			break
		}
	}
先通過lock加鎖。給nwait加一,表示有新的協程進入二叉樹,繼續對信號量sema的值進行驗證判斷,如果爲0,則繼續,否則嘗試減1並解鎖返回,之後通過queue方法將目標協程放入平衡二叉樹中等待。

首先把當前協程存入該節點的g指針中,並保存當前信號量地址。如果是第一次根據新的信號量而加入的節點,那麼會直接加入平衡二叉樹中,並調整樹。

s.ticket = fastrand() | 1
s.parent = last
*pt = s

// Rotate up into tree according to ticket (priority).
for s.parent != nil && s.parent.ticket > s.ticket {
   if s.parent.prev == s {
      root.rotateRight(s.parent)
   } else {
      if s.parent.next != s {
         panic("semaRoot queue")
      }
      root.rotateLeft(s.parent)
   }
}

如果不是第一次的插入,那麼首先根據信號量的地址從平衡二叉樹根節點開始尋找對應的信號量地址所綁定的節點,通過信號量地址的大小不斷找左右孩子節點,直到找到。

for t := *pt; t != nil; t = *pt {
   if t.elem == unsafe.Pointer(addr) {...}
   last = t
   if uintptr(unsafe.Pointer(addr)) < uintptr(t.elem) {
      pt = &t.prev
   } else {
      pt = &t.next
   }
}

找到之後,判斷下lifo(將協程準備阻塞之前會判斷以等待時間,如果不爲0,則lifo爲true,說明該協程已經進入過該平衡二叉樹)。當lifo爲true,則將新生成的節點取代原本節點在平衡二叉樹的位置,並將老節點放置在該信號量綁定節點的等待隊列的頭部。若lifo爲false,表示第一次,則把新的節點放在等待隊列的末尾。

if lifo {
   // Substitute s in t's place in treap.
   *pt = s
   s.ticket = t.ticket
   s.acquiretime = t.acquiretime
   s.parent = t.parent
   s.prev = t.prev
   s.next = t.next
   if s.prev != nil {
      s.prev.parent = s
   }
   if s.next != nil {
      s.next.parent = s
   }
   // Add t first in s's wait list.
   s.waitlink = t
   s.waittail = t.waittail
   if s.waittail == nil {
      s.waittail = t
   }
   t.parent = nil
   t.prev = nil
   t.next = nil
   t.waittail = nil
} else {
   // Add s to end of t's wait list.
   if t.waittail == nil {
      t.waitlink = s
   } else {
      t.waittail.waitlink = s
   }
   t.waittail = s
   s.waitlink = nil
}
return

將本次協程加入二叉樹中的隊列後,就可以把semRoot中的lock解鎖,並將當前協程阻塞。

回到最開始的Mutex的Lock()方法中,繼續看

starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			if 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 = true
			iter = 0
		} else {
			old = m.state
		}
	}

	if race.Enabled {
		race.Acquire(unsafe.Pointer(m))
	}
當執行後面代碼時,說明該協程已經被喚醒。先統計阻塞的時長,若超過1ms,則把starvating設爲true,就會在下次循環中將鎖的模式改爲starvation模式。如果此時已經是starvation模式,則把state存儲的阻塞線程數減1,如果此時starving爲false(阻塞時長小於1ms),或者阻塞協程數爲1(此時只有本協程一個佔用鎖),則從starvation轉爲normal模式。


解鎖我們來看下Unlock()
func (m *Mutex) Unlock() {
	if race.Enabled {
		_ = m.state
		race.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 := new
		for {
			// 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) | mutexWoken
			if 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)
	}
}

首先把state的最低位設爲0,表示已經解鎖。然後根據模式,如果是normal模式下,如果當前沒有阻塞協程,或者當前有協程在自旋獲得鎖,那麼可以直接返回。否則,更改state,阻塞協程數減一,且調用runtime_Semrelease方法喚醒協程,這裏協程不一定立即獲得鎖,鎖的競爭仍在。如果是處於starvation模式下,則直接調用runtime_Semrelease喚醒協程,這裏的喚醒協程持有鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章