golang lock_sema 源码分析

1,663 阅读3分钟

在看 golang sync.Mutex 锁时看到调用 sema 来实现 gorutine 的锁定,而 sema 调用 lock_sema 对 m 进行 park 和唤醒,就对 lock_sema 的源码了解了一波

lock

先从寄存器或者 TLS 里获取当前 g,继而获取 m,cas 抢占锁,cas 成功,说明抢到锁,直接返回。

每个 m 都有自己的 pthread_mutex 和 pthread_cond ,用来给自己加锁解锁等待和唤醒。

当 cas 失败,会空转自旋 4 次,四次都没有获取到锁,第五次会睡眠一毫秒,让出 cpu 执行时间,防止一直占用 cpu 用来空转

上面五次操作进行完,还是没有获取到锁,就会加入队列,并且调用 m 的 pthread_mutex 和 pthread_cond 把 m 挂起并且等待,队列是先入后出。

直到被唤醒,因为加入到队列时,存储的是 m 的指针,所以当前 m 唤醒队列中的 m 时,直接通过指针获取到 m, 同样调用 m 的 pthread_cond 把 m 唤醒和解锁,m 再去竞争锁

lock_sema 不支持重入,所以不能在同一 g 中重复加锁

unlock

解锁就很简单,不解释了,下面直接贴注释过的代码

代码

func lock(l *mutex) {
	gp := getg()
	if gp.m.locks < 0 {
		throw("runtime·lock: lock count")
	}
	gp.m.locks++

	// Speculative grab for lock.
	// 直接 cas 就锁上了
	if atomic.Casuintptr(&l.key, 0, locked) {
		return
	}
	semacreate(gp.m)

	// On uniprocessor's, no point spinning.
	// On multiprocessors, spin for ACTIVE_SPIN attempts.
	// 在单处理器上没有意义,多处理器上,自旋最多 ACTIVE_SPIN
	spin := 0
	if ncpu > 1 {
		spin = active_spin
	}
Loop:
	for i := 0; ; i++ {
		v := atomic.Loaduintptr(&l.key)
		// 锁释放了
		if v&locked == 0 {
			// Unlocked. Try to lock.
			if atomic.Casuintptr(&l.key, v, v|locked) {
				return
			}
			i = 0
		}
		if i < spin {
			// 空转三十次
			procyield(active_spin_cnt)
		} else if i < spin+passive_spin {
			// 让出CPU,实现是睡眠一微秒
			osyield()
		} else {
			// Someone else has it.
			// l->waitm points to a linked list of M's waiting
			// for this lock, chained through m->nextwaitm.
			// Queue this M.
			for {
				// 指向占有锁的线程 m
				gp.m.nextwaitm = muintptr(v &^ locked)
				// cas 设置锁标志,只是为了让确定顺序,让下一个线程指向自己,进队列,先进后出
				if atomic.Casuintptr(&l.key, v, uintptr(unsafe.Pointer(gp.m))|locked) {
					break
				}
				// cas 不成功说明锁标志被改了,有可能被别的线程占了,有可能释放了,重新获取锁标志
				v = atomic.Loaduintptr(&l.key)
				// 锁被释放了,继续循环
				if v&locked == 0 {
					continue Loop
				}
			}
			// 被锁住了
			if v&locked != 0 {
				// Queued. Wait.
				semasleep(-1)
				// 被唤醒之后, 继续抢锁
				i = 0
			}
		}
	}
}

//go:nowritebarrier
// We might not be holding a p in this code.
func unlock(l *mutex) {
	gp := getg()
	var mp *m
	for {
		v := atomic.Loaduintptr(&l.key)
		// 只有当前线程竞争
		if v == locked {
			if atomic.Casuintptr(&l.key, locked, 0) {
				break
			}
		} else {
			// Other M's are waiting for the lock.
			// Dequeue an M.
			// 获取标志位中存储的 m
			mp = muintptr(v &^ locked).ptr()
			// 出队列
			if atomic.Casuintptr(&l.key, v, uintptr(mp.nextwaitm)) {
				// Dequeued an M.  Wake it.
				semawakeup(mp)
				break
			}
		}
	}
	gp.m.locks--
	if gp.m.locks < 0 {
		throw("runtime·unlock: lock count")
	}
	if gp.m.locks == 0 && gp.preempt { // restore the preemption request in case we've cleared it in newstack 恢复抢占请求,以防我们在新堆栈中清除了它
		gp.stackguard0 = stackPreempt
	}
}
//go:nosplit
func semasleep(ns int64) int32 {
	var start int64
	if ns >= 0 {
		start = nanotime()
	}
	mp := getg().m
	// 互斥锁锁定
	pthread_mutex_lock(&mp.mutex)
	for {
		if mp.count > 0 {
			mp.count--
			pthread_mutex_unlock(&mp.mutex)
			return 0
		}
		if ns >= 0 {
			spent := nanotime() - start
			if spent >= ns {
				pthread_mutex_unlock(&mp.mutex)
				return -1
			}
			var t timespec
			t.setNsec(ns - spent)
			err := pthread_cond_timedwait_relative_np(&mp.cond, &mp.mutex, &t)
			if err == _ETIMEDOUT {
				pthread_mutex_unlock(&mp.mutex)
				return -1
			}
		} else {
			// 被唤醒之后继续 for 循环, 此时 mp.count 已经大于0了
			pthread_cond_wait(&mp.cond, &mp.mutex)
		}
	}
}

//go:nosplit
func semawakeup(mp *m) {
	pthread_mutex_lock(&mp.mutex)
	mp.count++
	if mp.count > 0 {
		pthread_cond_signal(&mp.cond)
	}
	pthread_mutex_unlock(&mp.mutex)
}