整体把握

可以参考以下文章,对golang的GMP调度器和GC有一个整体的认识和把握:
详解golang的调度器-GMP模型
详解golang中的GC

从实例出发

看下面一段代码,分析为什么为出现卡死的情况
运行环境基于go1.13


//使用golang版本为1.14之前,如1.13等
package main

import (
    "fmt"
    "runtime"
)

func main() {
    var i byte
    go func() {
        for i = 0; i <= 255; i++ {
        }
    }()
    fmt.Println("Dropping mic")
    // Yield execution to force executing other goroutines
    runtime.Gosched()
    runtime.GC()
    fmt.Println("Done")
}

Golang 中,byte 其实被 alias 到 uint8 上了。所以上面的 for 循环会始终成立,因为 i++ 到 i=255 的时候会溢出,i <= 255 一定成立。
也即是, for 循环永远无法退出,所以上面的代码其实可以等价于这样:

go func() {
    for {}
}
1.14版本之前
  • IO 操作
  • Channel 阻塞
  • system call
  • 运行较长时间(10ms)

如果一个 goroutine 执行时间太长,scheduler 会在其 G 对象上打上一个标志( preempt),当这个 goroutine 内部发生函数调用的时候,会先主动检查这个标志,如果为 true 则会让出执行权。
main 函数里启动的 goroutine 其实是一个没有 IO 阻塞、没有 Channel 阻塞、没有 system call、没有函数调用的死循环。

也就是,它无法主动让出自己的执行权,即使已经执行很长时间,scheduler 已经标志了 preempt。
而 golang 的 GC 动作是需要所有正在运行 goroutine 都停止后进行的。因此,程序会卡在 runtime.GC() 等待所有协程退出。

附:调度器发展历史

单线程调度器 · 0.x

  • 只包含 40 多行代码;
  • 程序中只能存在一个活跃线程,由 G-M 模型组成;

多线程调度器 · 1.0

  • 允许运行多线程的程序;
  • 全局锁导致竞争严重;

任务窃取调度器 · 1.1

  • 引入了处理器 P,构成了目前的 G-M-P 模型;
  • 在处理器 P 的基础上实现了基于工作窃取的调度器;
  • 在某些情况下,Goroutine 不会让出线程,进而造成饥饿问题;
  • 时间过长的垃圾回收(Stop-the-world,STW)会导致程序长时间无法工作;

抢占式调度器 · 1.2 ~ 至今

1基于协作的抢占式调度器 - 1.2 ~ 1.13

  • 通过编译器在函数调用时插入抢占检查指令,在函数调用时检查当前 Goroutine 是否发起了抢占请求,实现基于协作的抢占式调度;
  • Goroutine 可能会因为垃圾回收和循环长时间占用资源导致程序暂停;

2基于信号的抢占式调度器 - 1.14 ~ 至今

  • 实现基于信号的真抢占式调度;
  • 垃圾回收在扫描栈时会触发抢占调度;
  • 抢占的时间点不够多,还不能覆盖全部的边缘情况;

非均匀存储访问调度器 · 提案

  • 对运行时的各种资源进行分区;
  • 实现非常复杂,到今天还没有提上日程;
参考