golang mutex底层原理

数据结构

位于sync/mutex.go

type Mutex struct {
    state int32
    sema  uint32
}

state

locked:锁的状态,是否已经上锁

woken:表示是否有协程被唤醒,0表示没有协程被唤醒,1表示有协程被唤醒

starving:是否处于饥饿模式

waterCount:等待这把锁的协程数量,同一时刻只会有一个协程拿到锁,其余的都会处于阻塞等待中

sema

信号量,本质上是一个计数器

加锁流程

位于sync/mutex.go

func (m *Mutex) Lock() {
	// 先通过一次cas操作加锁,如果成功则获得锁,并返回
	if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
		// ...
		return
	}
	// 否则进入slow path处理逻辑
	m.lockSlow()
}
func (m *Mutex) lockSlow() {
	var waitStartTime int64
	starving := false
	awoke := false // 当前的 goroutine 是否是被唤醒的
	iter := 0 // 自旋次数
	old := m.state // 先记录一下老的状态
	for {
		// 如果锁被占用,且是正常模式,并且满足自旋条件,则可以自旋
		if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
            // 那么利用CAS,将 state 的唤醒标记置为1,标记自己是被唤醒的(在释放锁的时候,就不去等待)
			// 如果有其他 goroutine 已经设置了 state 的唤醒标记位,那么本次就会失败
			if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
				atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
				// awoke标记为true
				awoke = true
			}
			//自旋,执行没用的指令30次
			runtime_doSpin()
			iter++
			old = m.state
			continue
		}
		// 到这里,说明退出了自旋,当前锁没被占用 或者  系统处于饥饿模式 或者 自旋次数太多导致不符合自旋条件
		// ...
		// 锁被占用 或者 处于饥饿模式下,新增一个等待者
		if old&(mutexLocked|mutexStarving) != 0 {
			new += 1 << mutexWaiterShift
		}
		// ...
		if atomic.CompareAndSwapInt32(&m.state, old, new) {
			// 如果改状态之前,锁未被占用 且 处于正常模式,那么就相当于获取到锁了
			if old&(mutexLocked|mutexStarving) == 0 {
				break
			}
			// 到这里说明:1. 之前锁被占用  或者 2.之前是处于饥饿状态
			// 判断之前是否等待过(是否从队列里唤醒的),之前等待过,再次排队放在队首
			queueLifo := waitStartTime != 0
			// 如果之前没等过(新来的),设置等待起始时间当前时间
			if waitStartTime == 0 {
				waitStartTime = runtime_nanotime()
			}
			// 之前排过队的老人,放到等待队列队首;新人放到队尾,然后等待获取信号量
			runtime_SemacquireMutex(&m.sema, queueLifo, 1)
			// 锁被释放,goroutine 被唤醒
			// 设置当前 goroutine 饥饿状态,如果之前已经饥饿,或者距离等待开始时间超过了 1ms,也变饥饿
			starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
			old = m.state
			// 被唤醒后如果是饥饿状态
			if old&mutexStarving != 0 {
				// ...
				// 如果当前队列空了,就把starving清0了,将置为正常模式
				if !starving || old>>mutexWaiterShift == 1 {
					delta -= mutexStarving
				}
				atomic.AddInt32(&m.state, delta)
				// 退出循环,获得锁
				break
			}
			// 否则被唤醒后继续尝试自旋和其他协程竞争锁
			awoke = true
			iter = 0
		} else {
			old = m.state
		}
	}
	// ...
}

  1. 先通过atomic()方法尝试将state置为lock状态(本质上就是一个cas操作),如果成功则获取了锁,可以继续执行业务逻辑了。
  2. 如果没有设置成功,则尝试自旋,自旋的目的是乐观的认为在这段时间内锁被释放了,这样就可以快速地获取锁(如果是饥饿模式下就不执行自旋操作)。
  3. 如果自旋几次后还没有得到锁,会执行到SemacquireMutex ,就需要sleep原语来阻塞当前goroutine,并将其放入等待队列(FIFO):如果是新来的goroutine,就需要放在队尾;如果是被唤醒的等待锁的goroutine,就放在队头。之后等待解锁的goroutine发出信号进行唤醒。
  4. 唤醒之后的goroutine发现锁处于饥饿模式,则能直接拿到锁,否则重置自旋迭代次数并标记唤醒位,重新进入步骤2中

满足自旋的条件

需要同时满足以下几种条件:

1、mutex处于被锁状态

2、正常模式

3、sync_runtime_canSpin

// runtime/proc.go
func sync_runtime_canSpin(i int) bool {
   if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 {
      return false
   }
   if p := getg().m.p.ptr(); !runqempty(p) {
      return false
   }
   return true
}
  • 已经自旋的次数小于4次

  • 多核

  • 最大逻辑处理器大于1

  • 并且本地的P队列可运行G队列为空

解锁流程

func (m *Mutex) Unlock() {
	// ...
	new := atomic.AddInt32(&m.state, -mutexLocked)
	if new != 0 {
		m.unlockSlow(new)
	}
}
func (m *Mutex) unlockSlow(new int32) {
	if (new+mutexLocked)&mutexLocked == 0 {
		fatal("sync: unlock of unlocked mutex")
	}
	if new&mutexStarving == 0 {  // 如果是正常模式
		old := new
		for {
			// 如果队列中没有等待的goroutine或者有goroutine已经被唤醒并且抢占了锁(这种情况就如lock			// 中,正好处在cpu中的goroutine在自旋(自旋状态的goroutine的wake状态为1),那么就不需要			  // 再唤醒等待队列了
			if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
				return
			}
			// 如果队列中有等着的,并且也没有处在cpu中的goroutine去自旋获取锁,
			// 那么就抓住机会从等待队列中唤醒一个goroutine
			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)
	}
}
  1. 如果通过原子操作AddInt32后,锁变为完全空闲状态,则直接解锁。
  2. 如果锁处于正常模式,且没有goroutine等待锁释放,或者锁被其他goroutine设置为了锁定状态、唤醒状态中的任一种(非空闲状态),则会直接退出;否则,会通过wakeup原语Semrelease唤醒waiter。
  3. 如果锁处于饥饿模式,会直接将锁的所有权交给等待队列队头的goroutine,唤醒的goroutine会负责设置Locked标志位。

信号量

操作系统的信号量的管理对象是线程,而 Mutex 中使用的信号量是针对协程的,那么这就意味着golang需要重新实现一套基于协程的信号量。实际上sync.Mutex最终是依赖sema.go中实现的sleepwakeup原语来实现PV操作的。

数据结构

// src/runtime/sema.go

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

const semTabSize = 251

var semtable [semTabSize]struct {
	root semaRoot
	pad  [cpu.CacheLinePadSize - unsafe.Sizeof(semaRoot{})]byte
}

// src/runtime/runtime2.go

type sudog struct {
	g *g

	next *sudog
	prev *sudog
	elem unsafe.Pointer // data element (may point to stack)

	acquiretime int64
	releasetime int64
	ticket      uint32
	isSelect bool

	success bool

	parent   *sudog // semaRoot binary tree
	waitlink *sudog // g.waiting list or semaRoot
	waittail *sudog // semaRoot
	c        *hchan // channel
}
  1. 一个samtable有512个semaRoot;
  2. 一个semarRoot对应一个信号量量的PV操作。
  3. 一个semaRoot里有一个等待队列sudog,从它的nextpre 属性可以得知它支持链表,从它的parent属性可以得知它也支持树形结构。 channel的读写等待队列就是用的链表,而semaRoot使用的是平衡树。

阻塞

// src/sync/mutex.go
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// src/runtime/sema.go
func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) {
	semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes)
}

func semacquire1(addr *uint32, lifo bool, profile semaProfileFlags, skipframes int) {
	// 获取当前协程
	gp := getg()
	// ...
	// 做一次cas操作来尝试修改信号量的值,如果成功则拿到锁并返回
	if cansemacquire(addr) {
		return
	}
	// 构造sudog
	s := acquireSudog()
	root := semtable.rootFor(addr)
	t0 := int64(0)
	s.releasetime = 0
	s.acquiretime = 0
	s.ticket = 0
	// ...
	for {
		lockWithRank(&root.lock, lockRankRoot)
		// ...
		root.queue(addr, s, lifo) // 将sudog放入等待队列
		goparkunlock(&root.lock, waitReasonSemacquire, traceEvGoBlockSync, 4+skipframes)
        if s.ticket != 0 || cansemacquire(addr) {
			break
		}
	}
	// ...
	releaseSudog(s)
}
func lockWithRank(l *mutex, rank lockRank) {
	lock2(l)
}


func lock2(l *mutex) {
	gp := getg()
	// ...
	gp.m.locks++

	for {
		// ...
		futexsleep(key32(&l.key), mutex_sleeping, -1)
	}
}

  • 做一次cas操作来尝试修改信号量的值,如果成功则拿到锁并返回;
  • 构造封装当前 goroutine 的 sudog 对象;
  • 调用futexsleep()进入休眠,其最终发起操作系统底层的futex()调用;
  • 将sudog放入等待队列;
  • gopark 当前协程,进入gwaiting状态;

唤醒

有阻塞,就需要有对应的唤醒机制

// src/sync/mutex.go
runtime_Semrelease(&m.sema, false, 1)
// src/runtime/sema.go
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
	semrelease1(addr, handoff, skipframes)
}

func semrelease1(addr *uint32, handoff bool, skipframes int) {
	root := semtable.rootFor(addr)
	// ...
	s, t0 := root.dequeue(addr)
	// ...
	unlock(&root.lock)
	if s != nil {
        readyWithTime(s, 5+skipframes)
		// ...
		if s.ticket == 1 && getg().m.locks == 0 {
			goyield()
		}
	}
}

func unlock(l *mutex) {
	unlockWithRank(l)
}

func unlockWithRank(l *mutex) {
	unlock2(l)
}

func unlock2(l *mutex) {
	v := atomic.Xchg(key32(&l.key), mutex_unlocked)
	// ...
	if v == mutex_sleeping {
		futexwakeup(key32(&l.key), 1) // 唤醒阻塞的goroutine
	}

	// ...
}

func readyWithTime(s *sudog, traceskip int) {
	if s.releasetime != 0 {
		s.releasetime = cputicks()
	}
	goready(s.g, traceskip)
}
  • 从队列取出goroutine;
  • 调用futexwakeup()解除阻塞,其最终发起操作系统底层的futex()调用;
  • 执行goready(),将goroutine加入到本地P队列。
posted @ 2023-03-29 14:58  独揽风月  阅读(205)  评论(0编辑  收藏  举报