1 goroutine 的实现

1.1 GPM 模型

GPM 是 Go 调度器的三个核心组件。

1.1.1 Goroutine

G:Goroutine,有自己的栈、指向堆的指针,保存状态信息与寄存器值,用于当前 goroutine 被调度时使用。

需要注意的是,G 中还关联了两个结构体,分别为 stack 和 gobuf。

1
2
3
4
5
6
7
// 描述栈的数据结构,栈的范围:[lo, hi)
type stack struct {
    // 栈顶,低地址
    lo uintptr
    // 栈低,高地址
    hi uintptr
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 存储 goroutine 运行时寄存器的值
type gobuf struct {
    // 存储 rsp 寄存器的值
    sp   uintptr
    // 存储 rip 寄存器的值
    pc   uintptr
    // 指向 goroutine
    g    guintptr
    ctxt unsafe.Pointer // this has to be a pointer so that gc scans it
    // 保存系统调用的返回值
    ret  sys.Uintreg
    lr   uintptr
    bp   uintptr // for GOEXPERIMENT=framepointer
}

1.1.2 Machine

M:Machine,内核级线程,goroutine 需要调度到 M 上才能运行,M 是真正执行 G 的组件,维护 M 的栈信息、正在执行的 G 信息等。

1.1.3 Processor

P:Processor,为 M 的执行提供上下文,保存 M 执行 G 的资源,维护需要被执行的处于 runnable 状态的 goroutine 队列。

  • LRQ:本地可用 goroutine 队列。
  • GRU:全局可用 goroutine 队列。

当 P 发现 LRQ 为空时,会检查 GRQ 是否为空,不为空则从中获取 G 运行;否则从其他 P 中“偷取” G 执行。

1.1.4 工作模型

每次 go func,都会有一个新的协程被添加到队列结尾。

62031928-02a8f880-b21b-11e9-96a9-96820452463e1075473-20180704160300058-287296807

// TODO:补充 GPM 的内容


1.2 goroutine 与 thread

GoroutineThread
Goroutine 由 go runtime 管理thread 由 kernal 管理。
Goroutine 是用户级的thread 是内核级的
Goroutine 有可生长的分段堆栈。thread 没有可生长的堆栈。
goroutine 只需要保存三个寄存器thread 切换保存多种寄存器
goroutine 切换更快thread 切换慢
goroutine 间可以通过 channel 通信thread 间通信延迟高

1.3 go scheduler

Go 程序的执行分为两层:Go program 和 runtime,即用户程序和运行时,它们通过函数调用来实现内存管理、channel 通信、goroutine 创建等功能。用户程序进行系统调用会被 runtime 拦截,协助其进行调度与 GC 执行工作。

runtime 维护所有 goroutine,并通过 scheduler 进行调度

  1. go scheduler 使用 M:N 模型,在任一时刻,M 个 G(oroutine) 分配到 N 个 M(achine) 中,在若干个 P 上运行。
  2. 每个 M 都必须依附于一个 P,每个 P 可以挂载多个 M,同时只能运行一个 M。
  3. go schedule 每一轮调度都需要找到一个 runnable 的 G,并执行。

goroutine 是 go runtime 的一部分,与 go 程序一起运行,不受用户控制。


2 goroutine 的细节

2.1 调度时机

在以下四种情况,goroutine 可能发生调度。

  • 使用 go 关键字:创建一个新的 goroutine,Go scheduler 可能调度。
  • GC:GC 工作时也依赖 goroutine 进行调度。
  • 系统调用:当 goroutine 进行系统调用时,会阻塞 M,此时被调度走;同时一个新的 goroutine 会被调度上来。
  • 内存同步访问:atomic,mutex,channel 操作等会使 goroutine 阻塞,此时被调度走;等条件满足后(例如其他 goroutine 解锁了)会被调度上来继续运行。