固定链接 浅谈 Goroutine 调度

浅谈 Goroutine 调度

浅谈 Goroutine 调度

Go 语言再次拿下 TIBOE 年度编程语言,充分证明 Go 语言在业内的受欢迎程度。

Go 语言的高并发是大多数人选择 Go 的一大原因,下面就来介绍 Go 语言高并发调度是如何实现的。

Go 由 Runtime 对 Goroutine 进行调度,即何时决定调度哪个 Goroutine 开始执行,哪个 Goroutine 应该停止执行让出资源、哪个 Goroutine 应该被唤醒恢复执行等。

Go Runtime

Go Runtime 管理着 Goroutine 的调度以及运行时的垃圾回收。

Go 程序可以被看成两层,一层是用户代码,一层是 Runtime, 并且调用一些管理 Goroutine, Channel, 以及一些其他的高层抽象。

G P M 调度模型

Go 中的调度模型称为 G P M 调度模型。

Runtime 调度器通过把 Goroutine 绑定到操作系统线程来运行它们。Goroutine 可以看作是轻量级的线程。每个 Goroutine 用 G 来表示,它包含了用来跟踪栈的字段和当前状态。

Runtime 跟踪每一个 G 并且把它们绑定到 P(Logical Processor)。P可以被看作抽象资源或者上下文,操作系统线程用 M 来表示(OS Thread)需要获取它以便来执行 G。程序中可以通过 runtime.GOMAXPROCS ( numLogicalProcessors ) 来调整 P(Logical Processors)。

每一个 G(Goroutine)需要绑定到 P(Logical Processor)才能调度执行;就像每一个 User-level Thread 绑定到 Kernel Thread 才能被调度执行。

G – Goroutine

对于一个 Goroutine ,每个结构体 G 中有一个 sched 的属性就是用来保存它上下文的。这样,Goroutine 就可以很轻易的来回切换。

由于其上下文切换在用户态下发生,根本不必进入内核态,所以速度很快,而且只有当前 Goroutine 的 PC, SP 等少量信息需要保存。

  • 内存
    • Goroutine 的创建大约 2KB 栈空间
    • 线程一创建就要约 1MB 内存
  • 切换
    • 线程切换,调度器需要保存所有的寄存器,通用寄存器、程序计数器、栈指针
    • 保存 3 个寄存器:程序计数寄存器 PC、栈指针 SP 以及 DX

创建一个 Goroutine 并准备运行,这个 Goroutine 会被放到调度器的 Global 队列中。然后调度器就将这些队列中的 Goroutine 分配给一个P(Logical Processor),并放到这个 P 对应的 Local 队列中。Local 队列中的 Goroutine 会一直等待直到自己被分配的 P 执行。

如果正在运行的 Goroutine 被一个系统调用阻塞,如打开一个文件。当这种情况发生时,M2(OS Thread)和 Goroutine 会从 P0(Logical Processor)上分离,这个 M2 会一直阻塞直到系统调用返回。

与此同时,P0 就失去了用来运行的 M2。所以,调度器会创建一个新的 M3,并将其绑定到 P0 上。之后,调度器会从 Local 队列中选择另外一个 Goroutine 运行。一旦刚才阻塞的系统调用执行完毕并返回,对应的 Goroutine 会放回到 Local 队列。

M – OS Thread

  • M:代表一个线程

M 会从运行队列中取出 G , 然后运行 G , 如果 G 运行完毕或者进入休眠状态,则从运行队列中取出下一个 G 运行,周而复始。

有时候 G 需要调用一些无法避免阻塞的原生代码,这时 M 会释放持有的 P 并进入阻塞状态,其他 M 会取得这个 P 并继续运行队列中的 G。

P – Logical Processor

  • P:调度的上下文
  • runqueue: 等待的 Goroutine

work stealing算法

需要维护多个 P,因为当一个 M 被阻塞时,P 可以转而投奔另一个 M。

P 所分配的任务 G 很快就执行完了(分配不均),这就导致了一个 P 空闲而系统却任然忙碌。但是如果 global runqueue 没有任务 G 了,那么 P 就不得不从其他的上下文 P 那里拿一些 G 来执行。一般来说,如果上下文 P 从其他的上下文 P 那里要偷一个任务的话,一般就‘偷’ run queue 的一半。

本文作者:尹长昕

您的留言将激励我们越做越好