LockOSThread实现原理
in Go with 0 comment

LockOSThread实现原理

in Go with 0 comment

$GOROOT\src\runtime\proc.go

runtime.LockOSThread()
runtime.UnlockOSThread()

其实注释中比较详细了,这里简化说下

func LockOSThread() {
	if atomic.Load(&newmHandoff.haveTemplateThread) == 0 && GOOS != "plan9" {
		startTemplateThread()
	}
	_g_ := getg()
    // 将M的lockedExt字段计数加一
	_g_.m.lockedExt++
	if _g_.m.lockedExt == 0 {
		_g_.m.lockedExt--
		panic("LockOSThread nesting overflow")
	}
	dolockOSThread()
}

func dolockOSThread() {
	if GOARCH == "wasm" {
		return // no threads on wasm yet
	}
	_g_ := getg()
    // 设置M的lockedg字段为G
	_g_.m.lockedg.set(_g_)
    // 设置G的lockedm字段为M
	_g_.lockedm.set(_g_.m)
}

func UnlockOSThread() {
	_g_ := getg()
    // 如果当前没有被锁定,直接返回
	if _g_.m.lockedExt == 0 {
		return
	}
    // 将M的lockedExt字段计数减一
	_g_.m.lockedExt--
	dounlockOSThread()
}

func dounlockOSThread() {
	if GOARCH == "wasm" {
		return // no threads on wasm yet
	}
	_g_ := getg()
    // 如果lockedExt仍有绑定计数时,也直接返回(多次LockOSThread,没有完全解锁)
	if _g_.m.lockedInt != 0 || _g_.m.lockedExt != 0 {
		return
	}
	_g_.m.lockedg = 0	// 清空M中的lockedg标志
	_g_.lockedm = 0		// 清空G中的lockedm标志
}

继续分析,如果做到“G只能在这个M上运行,这个M也只能运行这个G”

LockOSThread时设置了两个状态 m.lockedg 、 g.lockedm,在schedule中会使用这两个字段进行判断

func schedule() {
	if _g_.m.lockedg != 0 {	// 如果一个 LockOSThread 的M进入调度
		stoplockedm()	// 停止当前M,“死等”与其绑定的G
		execute(_g_.m.lockedg.ptr(), false) // 执行与其绑定的G.
	}
    ……

top:
    ……
	if gp.lockedm != 0 { // 如果一个 LockOSThread的G进入调度
		// 将当前M的P交出给 与G绑定的M,去运行这个G,唤醒与G绑定的M,然后停止当前M
		startlockedm(gp)
		goto top
	}

	execute(gp, inheritTime)
}

func stoplockedm() {
	_g_ := getg()
	if _g_.m.p != 0 {
		// Schedule another M to run this p.
		_p_ := releasep()
		handoffp(_p_)
	}
	incidlelocked(1)
	// Wait until another thread schedules lockedg again.
	notesleep(&_g_.m.park)	// 等待被唤醒
    ------------------------------------
	noteclear(&_g_.m.park)	// 被唤醒
	status := readgstatus(_g_.m.lockedg.ptr())
	acquirep(_g_.m.nextp.ptr())	// 获取P
	_g_.m.nextp = 0
    // 回到调度,执行与其绑定的G
}

func startlockedm(gp *g) {
	_g_ := getg()

	mp := gp.lockedm.ptr()
	// directly handoff current P to the locked m
	incidlelocked(-1)
	_p_ := releasep()
	mp.nextp.set(_p_)	// 将当前P直接传递给 与G绑定的M,让其去运行
	notewakeup(&mp.park) // 唤醒与G绑定的M(在上方 notesleep(&_g_.m.park) 中等待)
	stopm()
}

如何做到 “如果处于LockOSThread的G退出了,那么与其绑定的M会被退出”
这里参考 qcrao的一篇文章千难万险 —— goroutine 从生到死(六)

在创建G的时候,会在栈底埋入goexit,G退出时,会自动跳到goexit中,并进一步跳转到goexit0,正常情况下会再次进入调度循环。

func goexit0(gp *g) {
	_g_ := getg()
	locked := gp.lockedm != 0 // 先缓存G是否被 LockOSThread
	gp.lockedm = 0	// 清空LockOSThread标志
	_g_.m.lockedg = 0 // 清空LockOSThread标志
    ……
	dropg()

	gfput(_g_.m.p.ptr(), gp)
	if locked {
        // 如果是G退出时仍处于LockOSThread状态
        // 直接让M退出,而不是进入runtime调度线程池(这里应该只是个策略选择问题,这个M中可能仍缓存了一些 “每线程独立状态”,所以让其退出避免复用时出现冲突)
        // 调用gogo(&_g_.m.g0.sched) 后,会回到 启动M时的mstart函数,进而触发结束当前M的流程
		if GOOS != "plan9" { // See golang.org/issue/22227.
			gogo(&_g_.m.g0.sched)
		} else {
			// Clear lockedExt on plan9 since we may end up re-using
			// this thread.
			_g_.m.lockedExt = 0
		}
	}
	schedule()
}