Go笔记:GMP

1
2
3
4
5
6
7
8
func main() {
runtime.GOMAXPROCS(1)
go func() {
for {}
}()
time.Sleep(time.Second)
fmt.Println("hello go!")
}

执行上面代码,是执行第一个go func的无限循环,还是打印”hello go!”?

协程

由于 goroutine,Go 可以轻松地同时运行数万个。在最初的计算机时代,只有进程,进程的缺点更加明显,它们太重了。fork一个40M内存的进程,消耗了更多的资源。后来,出现了线程。初始化一个线程应该比进程少一个数据量级的内存,进程要轻得多。与进程相比,线程并发要好很多,但线程切换也得落入内核.如果频繁切换,这个开销还是比较大的。cpu要保存临时存储线程当前的运行状态等等。如果线程很多,CPU在上下文切换上浪费了太多时间。

后来出现了协程,用户协作。相对于线程的优势在于一个线程可以关联多个协程,避免创建过多线程,并且在一个协程执行完后,可以通知下一个协程执行,这样切换就不需要切换到内核模式。它可以在用户模式下完成。

goroutine的起源也来自协程,协程自然拥有协程的所有好处。当然,还有比传统协程更好的地方。创建一个goroutine只需要2k内存,线程切换会消耗大约1000-1500ns,也就是1ns。可以执行12-18条指令。goroutine切换只需要200ns左右,相比线程至少可以节省5倍的时间。一个操作系统级线程可以绑定到多个 goroutine。当发生goroutine切换时,只需要移除当前正在执行的goroutine,不需要进行线程切换。(goroutine不会直接占用cpu,执行超过一段时间就会放弃)

GMP

  • G代表一个goroutine,它包含:表示goroutine栈的一些字段,指示当前goroutine的状态,指示当前运行到的指令地址,也就是PC值
  • M表示内核线程,包含正在运行的goroutine等字段
  • P代表一个虚拟的Processor,它维护一个处于Runnable状态的g队列,m需要获得p才能运行g
  • 当然还有一个核心的结构体:sched,它总览全局

三者的关系:当M有P,P有G时,M才能执行G。

调度算法:

  1. 程序启动时,先初始化并检查P的个数
  2. 创建 goroutine 时,首先尝试将其放入本地队列。如果本地队列已满,则本地队列的前半部分和这个新的 goroutine 将被移动到全局队列中
  3. 如果没有 P 可用,则将新的 goroutine 添加到全局队列中
  4. 如果得到一个空闲的P,则尝试唤醒一个M,当没有可用的M时再创建一个新的M
  5. 当 M 与 P 相关联,并且本地队列中有任务时,它可以直接从 p 的本地队列中取出 goroutine 执行
  6. 当P的本地队列中没有goroutine时,它会尝试从全局队列中取出一部分放入本地队列。该进程被锁定
  7. 当它没有从全局队列中取出时,它会尝试从其他 P 的本地队列中窃取一半并将其放入自己的本地队列中
  8. 当 G 发生系统调用时,P 会与当前 M 断开连接,并尝试从 M 的空闲队列中获取 M 以继续执行剩余的 goroutine
  9. 当上述 G 系统调用结束时,M 尝试获取一个 P 继续执行,如果没有,则将 g 放入全局队列,并自行进入 M 的空闲队列。这不是为了销毁M,是为了避免以后再创建M,造成不必要的开销

goroutine的优点是每个g不直接占用m

每个 M 初始化时,都会注册一个可以接收 sigurg 信号的处理程序。此 sigurg 信号由 sysmon 监视器发送。sysmon 占用单个 M,sysmon 会检查 goroutine 是否执行超过 10ms 或者是否执行了 GC(STW)。如果条件满足,sysmon会给对应的M发送sigurg信号,对应的handler开始执行,标记正在执行的G,然后判断当前栈是否溢出(morestack)。满足条件后,M会保存当前G的上下文(如果下次这个G还能被这个M执行时,可以通过上下文快速返回到上次执行位置),当前G会被丢弃进入全局 G 队列,而 M 继续执行下一个 G 。

如何执行文章开头代码:

在程序开头设置p=1,那么就没有并行问题了,只有一个goroutine可以同时执行,要么是go func的无限循环,要么是main goroutine的time.sleep以后。

  • go1.14以下的版本会无限循环卡住,因为即使main goroutine抢到P,也会因为sleep放弃cpu,然后go func会开始无限执行
  • go1.14及以上,由于sysmon抢占,即使第一个go func抢到P,也会因为执行时间超过10ms而被踢出P的本地队列。执行main goroutine的打印,然后不卡住就退出

PS: go1.14以后,goroutine支持信号抢占式调度