Contents

Goroutine调度方式

关于GMP模型

  • 就很符合人类思考的一中设计吧;G负责发起,P负责安排,M负责执行
  • 1.1版本才有的P,使M可以专注于执行
  • 再加点工作窃取之类的技巧,压榨下cpu
  • work stealing 机制:当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程
  • hand off 机制:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行
  • Go 调度本质是把大量的 goroutine 分配到少量线程上去执行,并利用多核并行,实现更强大的并发

调度方式

协作式

主动调度弃权:抢占标记

  • 在函数调用的序言插入抢占检测指令,若检测到标记,则主动中断执行,让出执行权利
可以被抢占的条件
  • 运行时没有禁止抢占(m.locks == 0)
  • 运行时没有在执行内存分配(m.mallocing == 0)
  • 运行时没有关闭抢占机制(m.preemptoff == “")
  • M 与 P 绑定且没有进入系统调用(p.status == _Prunning)
  • stackguard0 设置抢占标记 stackPreempt 的时机
Note
  • 进入系统调用时(runtime.reentersyscall,注意这种情况是为了保证不会发生栈分裂, 真正的抢占是异步的通过系统监控进行的)
  • 任何运行时不再持有锁的时候(m.locks == 0)
  • 当垃圾回收器需要停止所有用户 Goroutine 时
  • 存在问题:若协程中无函数,则无法设置抢占标记,也就无法被抢占
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 此程序在 Go 1.14 之前的版本不会输出 OK
package main
import (
	"runtime"
	"time"
)
func main() {
	runtime.GOMAXPROCS(1)
	go func() {
		for {
		}
	}()
	time.Sleep(time.Millisecond)
	println("OK")
}

主动用户让权:Gosched

  • 使用示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"fmt"
	"runtime"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		runtime.Gosched()
		fmt.Println(s)
	}
}

func TestSched() {
	runtime.GOMAXPROCS(1)
	go say("world")
	say("hello")
}

func main() {
	TestSched()
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
➜  p10 go run main.go
hello
world
hello
world
hello
world
hello
world
hello
  • 如果去掉 runtime.GOMAXPROCS(1) 或者 runtime.Gosched() 则打印不出来全部输出
Note
  • 若有多个P,则go出来的协程会挂到别的P上
  • 当只有一个P时,会通过循环中的 runtime.Gosched() 轮流让出 执行机会

抢占式

异步系统调用

  • 比如一个协程进行网络IO操作,会被当前的MP抛给netpoller
  • 在完成IO操作后,等待队列中的 G 会被唤醒,标记为可运行(runnable),并被放入到某 P 的队列中(抢P),绑定一个 M 后继续执行

同步系统调用

  • 比如文件 I/O,MG 会和P分离(P另寻M,抢M)
  • 当M从系统调用返回时,不会继续执行,而是将G放到run queue

被动 GC 抢占

  • 当需要进行垃圾回收时,为了保证不具备主动抢占处理的函数执行时间过长,导致垃圾回收迟迟不得执行而导致的高延迟,而强制停止 G 并转为执行垃圾回收

抢占信号的选取

  • 有关信号
  • preemptM 完成了信号的发送,直接向需要进行抢占的 M 发送 SIGURG 信号 即可
  • 为什么是 SIGURG 信号而不是其他的信号?如何才能保证该信号 不与用户态产生的信号产生冲突?是因为:
Note
  • 默认情况下,SIGURG 已经用于调试器传递信号。
  • SIGURG 可以不加选择地虚假发生的信号。例如,我们不能选择 SIGALRM,因为 信号处理程序无法分辨它是否是由实际过程引起的(可以说这意味着信号已损坏)。 而常见的用户自定义信号 SIGUSR1 和 SIGUSR2 也不够好,因为用户态代码可能会将其进行使用
  • 需要处理没有实时信号的平台(例如 macOS)
  • 考虑以上的观点,SIGURG 其实是一个很好的、满足所有这些条件、且极不可能因被用户态代码 进行使用的一种信号。

两个特殊的M

  • 一个名为 NetPoller 的 M 异步处理网络IO,它不需要和 P 进行绑定。当 G 执行网络 IO 的时候,G 会将当前 M 和 P 解绑,进入到 NetPoller 的 M 中,等待网络 IO 完成,这样即使执行网络 IO 的系统调用,也不会产生阻塞的 M.当网络 IO 完成后,M 的 Schedule 函数,会通过 findrunable函数 取到这个 G,继续运行它
  • 一个名为 sysmon 的 M的系统监控线程,它也不需要绑定 P 就可以运行(以 g0 这个 G 的形式),它的主要功能有:
Note
  • 检查死锁runtime.checkdead
  • 运行计时器 — 获取下一个需要被触发的计时器;
  • 将长时间未处理的 netpoll 结果添加到任务队列;
  • 向长时间运行的 G 任务发出抢占调度(retake 方法);Go 的抢占式调度当 sysmon 发现 M 已运行同一个 G(Goroutine)10ms 以上时,它会将该 G 的内部参数 preempt 设置为 true。然后,在函数序言中,当 G 进行函数调用时,G 会检查自己的 preempt 标志,如果它为 true,则它将自己与 M 分离并推入“全局队列”。由于它的工作方式(函数调用触发),在 for{} 的情况下并不会发生抢占,如果没有函数调用,即使设置了抢占标志,也不会进行该标志的检查。Go1.14 引入抢占式调度(使用信号的异步抢占机制),sysmon 仍然会检测到运行了 10ms 以上的 G(goroutine)。然后,sysmon 向运行 G 的 P 发送信号(SIGURG)。Go 的信号处理程序会调用P上的一个叫作 gsignal 的 goroutine 来处理该信号,将其映射到 M 而不是 G,并使其检查该信号。gsignal 看到抢占信号,停止正在运行的 G。
  • 打印调度信息,归还内存等定时任务.
  • 释放闲置超过 5 分钟的 span 内存;如果超过 2 分钟没有垃圾回收,强制执行;
  • 收回因 syscall 长时间阻塞的 P

本文参考

coffee