阅读 876

【Go并发编程】第一篇 - Goroutines调度

进程和线程

当运行一个应用程序的时候,操作系统会给这个应用程序启动一个进程。我们可以将进程看作一个包含应用程序在运行中需要用到和维护的各种资源的容器。一个进程至少包含一个线程,这个线程就是主线程。操作系统会调度线程到不同的CPU上执行,这个CPU不一定就是进程所在的CPU。

  • 进程:资源的所有权
  • 线程:执行和调度的基本单位
  • 同一进程下的各个线程共享资源,但寄存器、栈、PC不共享

Go调度

基本术语

Go Runtime管理调度,垃圾收集和Goroutine的运行时环境。这里我们只谈调度器。

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

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

G-P-M调度模型

Go 1.1中实现了G-P-M调度模型和work stealing算法,这个模型一直沿用至今:

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

M、P 和 G 之间的交互

创建一个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队列。

并发(Concurrency)不是并行(Parallelism)

上图解释了并发和并行的区别 并发 - 一个咖啡机同时服务两个队列 并行 - 两个咖啡机服务同时服务两个队列

如果希望让Goroutine并行,必须使用多于一个P(Logical Processor)。当有多个P时,调度器会将Goroutine平等分配到每个P上。这会让Goroutine在不同M(OS Thread)上运行。不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。

参考资料。

  1. Go In Action
  2. 也谈goroutine调度器
  3. Go scheduler: Ms, Ps & Gs
文章分类
阅读