1. goroutine的主动调度是指当前正在运行的goroutine通过直接调用runtime.Gosched()函数暂时放弃运行而发生的调度
  2. 主动调度完全是【用户代码】自己控制的,我们根据代码就可以预见什么地方一定会发生调度。
  3. 使用示例:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "runtime"
    "sync"
)

const N = 1

func main() {
    var wg sync.WaitGroup

    wg.Add(N)

    for i := 0; i < N; i++ {
        go start(&wg)
    }

    wg.Wait()
}

func start(wg *sync.WaitGroup) {
    for i := 0; i < 1000; i++ {
        runtime.Gosched()   // 这里会让出调度
    }

    wg.Done()
}

Gosched()

  1. Gosched放弃当前CPU运行权限,允许其他goroutine运行。
  2. 它会挂起当前的goroutine,因此执行会自动恢复。
  3. 文件位置:go1.19.3/src/runtime/proc.go
314
315
316
317
318
319
320
321
322
323
324
325
// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
    // amd64 linux平台是空函数
    checkTimeouts()		
    
    // mcall 函数在 user goroutine 中已分析,切换到g0栈调用 gosched_m 函数。
    // 相关 user goroutine 寄存器信息在 mcall 函数中被保存了。
    mcall(gosched_m)
    
    // goroutine 再次被调度起来则从这里开始运行
}

gosched_m()

  1. 根据mcall()函数知道这里的gp *guser goroutine,不是g0
  2. 但是当前已经切换到g0栈了。
  3. 文件位置:go1.19.3/src/runtime/proc.go
3381
3382
3383
3384
3385
3386
3387
3388
3389
// Gosched continuation on g0.
func gosched_m(gp *g) {
    // traceback 不关注
    if trace.enabled {	
        traceGoSched()
    }
    // gp is user goroutine
    goschedImpl(gp)
}

goschedImpl()

  1. user goroutine修改状态后与M解绑,加入全局队列池中,开启下一轮调度循环。
  2. 文件位置:go1.19.3/src/runtime/proc.go
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
func goschedImpl(gp *g) {
    // readgstatus(gp) -> atomic.Load(&gp.atomicstatus)
    // 原子获取 user goroutine 的状态
    status := readgstatus(gp)
    // 判断 user goroutine 状态
    if status&^_Gscan != _Grunning {
        dumpgstatus(gp)
        throw("bad g status")
    }
    // 当前 user goroutine 即将被挂起,需要修改状态
    // _Grunning: goroutine可能正在运行用户代码,它的栈归自己所有。
    // _Grunnable:goroutine应该在某个runq中,当前并没有在运行用户代码,它的栈不归自己所有。
    casgstatus(gp, _Grunning, _Grunnable)
    // 设置当前m.curg = nil, gp.m = nil 解除m与g之前的绑定关系
    dropg()	
    lock(&sched.lock)
    // 把gp放入sched的全局运行队列runq
    globrunqput(gp)	
    unlock(&sched.lock)

    // 在schedule函数中会判断 m.locks != 0 判断
    // 因此当前M还存在锁情况下不要让出
    schedule()  // 进入新一轮调度
}

drop()

  1. dropg去除m和当前goroutine m->curg(简称gp)之间的关联。
  2. 通常,调用者会将gp的状态设置为不运行状态,然后立即调用dropg完成工作。调用者还负责安排gp在适当的时间使用ready重新启动。
  3. 在调用dropg并安排稍后准备好gp之后,调用者可以做其他工作,但最终应该调用schedule来重新启动这个m上的goroutine的调度。
  4. 文件位置:go1.19.3/src/runtime/proc.go
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
// dropg removes the association between m and the current goroutine m->curg (gp for short).
// Typically a caller sets gp's status away from Grunning and then
// immediately calls dropg to finish the job. The caller is also responsible
// for arranging that gp will be restarted using ready at an
// appropriate time. After calling dropg and arranging for gp to be
// readied later, the caller can do other work but eventually should
// call schedule to restart the scheduling of goroutines on this m.
func dropg() {
    _g_ := getg() // g0

    // gp.m = nil
    setMNoWB(&_g_.m.curg.m, nil)
    // m.curg = nil
    setGNoWB(&_g_.m.curg, nil)
}

setMNoWB()

  1. 文件位置:go1.19.3/src/runtime/runtime2.go
307
308
309
310
311
312
313
314
// setMNoWB performs *mp = new without a write barrier.
// For times when it's impractical to use an muintptr.
//
//go:nosplit
//go:nowritebarrier
func setMNoWB(mp **m, new *m) {
    (*muintptr)(unsafe.Pointer(mp)).set(new) // *mp = new
}

setGNoWB()

  1. 文件位置:go1.19.3/src/runtime/runtime2.go
273
274
275
276
277
278
279
280
// setGNoWB performs *gp = new without a write barrier.
// For times when it's impractical to use a guintptr.
//
//go:nosplit
//go:nowritebarrier
func setGNoWB(gp **g, new *g) {
    (*guintptr)(unsafe.Pointer(gp)).set(new) // *gp = new
}

globrunqput()

  1. gp放到全局可运行队列中。sched.lock必须被持有。
  2. 可能在STW期间运行,因此不允许写屏障。
  3. 文件位置:go1.19.3/src/runtime/proc.go
5563
5564
5565
5566
5567
5568
5569
5570
5571
5572
5573
5574
5575
// Put gp on the global runnable queue.
// sched.lock must be held.
// May run during STW, so write barriers are not allowed.
//
//go:nowritebarrierrec
func globrunqput(gp *g) {
    // sched.lock 锁必须被持有
    assertLockHeld(&sched.lock)

    // gp 加入全局队列中
    sched.runq.pushBack(gp)
    sched.runqsize++
}