返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

4.5.1 调度

发布于 2024-10-12 19:15:58 字数 4936 浏览 0 评论 0 收藏 0

调度函数除在各级队列查找任务外,还要处理垃圾回收、锁定等情况。

// proc.go

// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.

func schedule() {
    _g_ := getg()
    
    // 检查当前 M 是否被特定 G 锁定。
    // 交出 P,休眠 M,直到其他人发现锁定任务后唤醒执行。    
    if _g_.m.lockedg != 0 {
        stoplockedm()
        execute(_g_.m.lockedg.ptr(), false) // Never returns.
    }
    
top:
    
    pp := _g_.m.p.ptr()
    pp.preempt = false
    
    // STW 标记。
    if sched.gcwaiting != 0 {
        gcstopm()
        goto top
    }
    
    // 执行安全函数,比如 forEachP。
    if pp.runSafePointFn != 0 {
        runSafePointFn()
    }

    // 检查并执行定时器。
    checkTimers(pp, 0)
    
    var gp *g
    var inheritTime bool

    // 启动垃圾回收任务。
    if gp == nil && gcBlackenEnabled != 0 {
        gp = gcController.findRunnableGCWorker(_g_.m.p.ptr())
        if gp != nil {
            tryWakeP = true
        }
    }
    
    // 每隔 60 次,尝试从全局队列提取任务,以确保公平。
    if gp == nil {
        
        // Check the global runnable queue once in a while to ensure fairness.
        // Otherwise two goroutines can completely occupy the local runqueue
        // by constantly respawning each other.
        
        if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
            lock(&sched.lock)
            gp = globrunqget(_g_.m.p.ptr(), 1)
            unlock(&sched.lock)
        }
    }
    
    // 从本地任务队列提取。
    if gp == nil {
        gp, inheritTime = runqget(_g_.m.p.ptr())
        // We can see gp != nil here even if the M is spinning,
        // if checkTimers added a local goroutine via goready.
    }
    
    // 从其他可能的地方提取。
    if gp == nil {
        gp, inheritTime = findrunnable() // blocks until work is available
    }
    
    // 找到任务,解除自旋状态。
    if _g_.m.spinning {
        resetspinning()
    }

    // 找到锁定任务,移交 P 给锁定 M 去执行。
    // 自己休眠。一旦被唤醒,则重新开始调度循环。
    if gp.lockedm != 0 {
        
        // Hands off own p to the locked m,
        // then blocks waiting for a new p.
        startlockedm(gp)
        
        goto top
    }
    
    // 执行任务。
    execute(gp, inheritTime)
}

如返回的是 runnext,则 inheritTime = True 。表示继承上个任务时间片,不累加执行计数器(P.schedtick),这会减少对全局队列的检查。

另一方面,sysmon 会比对不同时段计数器,检查是否执行了新任务。以此判断任务执行时间是否过长(10 ms),是否需要抢占调度。

GODEBUG=scheddetail 输出里可以看到计数器信息。

单个 G 未必在一次调度内结束。因某些原因中断,被重新放回任务队列,等待断点恢复。

MP 切换任务前,会将执行状态(PC、SP)保存到 G.sched,其他寄存器数据保存到 G.stack。

切换本身(G100->G0->G101)消耗并不大,关键在于如何查找下一个 G 任务。

  • 阻塞:block,syscall、mutex、channel。
  • 抢占:preempt,sysmon。

锁定

某些 G (syscall、cgo)需要与特定 M 锁定。

一旦锁定,M 会休眠直到 G 出现,并执行。

同理,G 只能在锁定 M 上执行。

这么做是为了维持 M 的某些特殊状态(比如 signal 等)

// proc.go

// LockOSThread wires the calling goroutine to its current operating system thread.
// The calling goroutine will always execute in that thread,
// and no other goroutine will execute in it,
// until the calling goroutine has made as many calls to
// UnlockOSThread as to LockOSThread.
// If the calling goroutine exits without unlocking the thread,
// the thread will be terminated.
//
// All init functions are run on the startup thread. Calling LockOSThread
// from an init function will cause the main function to be invoked on
// that thread.
//
// A goroutine should call LockOSThread before calling OS services or
// non-Go library functions that depend on per-thread state.

func LockOSThread() {
	_g_ := getg()
	_g_.m.lockedExt++
	dolockOSThread()
}


func lockOSThread() {
	getg().m.lockedInt++
	dolockOSThread()
}
// dolockOSThread is called by LockOSThread and lockOSThread below
// after they modify m.locked. Do not allow preemption during this call,
// or else the m might be different in this function than in the caller.

func dolockOSThread() {
	_g_ := getg()
	_g_.m.lockedg.set(_g_)  // 存放位置!
	_g_.lockedm.set(_g_.m)
}

在调度函数头部,一旦检查到当前 M 被某个 G 锁定,那么会进入休眠状态,直到目标 G 出现。

// Stops execution of the current m that is locked to a g until the g is runnable again.
func stoplockedm() {
	_g_ := getg()

    // 交出 P 给其他 M 去执行任务。
	if _g_.m.p != 0 {
		// Schedule another M to run this p.
		_p_ := releasep()
		handoffp(_p_)
	}
    
	// Wait until another thread schedules lockedg again.
	mPark()

    // 被其他 MP 唤醒,关联它转交的 P。
	acquirep(_g_.m.nextp.ptr())
	_g_.m.nextp = 0
}

其他 MP 找到锁定 G 时,会交出自己的 P,唤醒该任务关联 M。

// Schedules the locked m to run the locked gp.
func startlockedm(gp *g) {
	_g_ := getg()

    // 该任务锁定的 M。
	mp := gp.lockedm.ptr()
    
	// 转交自己的 P 给锁定 M,并唤醒它。
	_p_ := releasep()
	mp.nextp.set(_p_)
	notewakeup(&mp.park)
    
    // 当前 M 休眠。
	stopm()
}

为什么等待别人转交,而不是锁定 M 自己找?MP 创建锁定 G,执行期间可能因为某些原因(syscall)暂停。此时,P 被转交给其他人去执行任务,当 M 醒来时,已失去原 P,也就不方便从其任务队列提取。更何况,该 G 还可能被转移到全局队列,或被偷窃。如果直接从 M.lockedg 提取,那么如何处理其存放队列?不如,等它出现后移交。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文