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唤醒协程,这里的唤醒协程持有锁。

猜你喜欢

转载自blog.csdn.net/panxj856856/article/details/80377850
今日推荐