Goroutine 的并发编程模型基于GMP模型。

并发 & 并行

随着服务器硬件迭代升级,配置也越来越高。为充分利用服务器资源,并发编程也变的越来越重要。在开始之前,需要了解一下并发和并行的区别。

  • 并发: 逻辑上具有处理多个同时性任务的能力。
  • 并行: 物理上同一时刻执行多个并发任务。

通常所说的并发编程,也就是说它允许多个任务同时执行,但实际上并不一定在同一时刻被执行。在单核处理器上,通过多线程共享CPU时间片串行执行(并发非并行)。而并行则依赖于多核处理器等物理资源,让多个任务可以实现并行执行(并发且并行)。

多线程或多进程是并行的基本条件,但单线程也可以用协程(coroutine)做到并发。简单将 Goroutine 归纳为协程并不合适,因为它运行时会创建多个线程来执行并发任务,且任务单元可被调度到其它线程执行。这更像是多线程和协程的结合体,能最大限度提升执行效率,发挥多核处理器能力。

Go编写一个并发编程程序很简单,只需要在函数之前使用一个Go关键字就可以实现并发编程。

func main() {
    go func() {
        fmt.Println("hello, world")
    }()
}

GMP 模型

Goroutine 调度器组成

G

G 是 Goroutine 的缩写,相当于操作系统中的进程控制块,在这里就是 Goroutine 的控制结构,是对Goroutine 的抽象。其中包括执行的函数指令及参数;G 保存的任务对象;线程上下文切换,现场保护和现场恢复需要的寄存器(SP、IP)等信息。

Go不同版本 Goroutine 默认栈大小不同。

M

M 是一个线程或称为Machine,所有 M 是有线程栈的。如果不对该线程栈提供内存的话,系统会给该线程栈提供内存(不同操作系统提供的线程栈大小不同)。当指定了线程栈,则 M.stack → G.stack,M 的 PC 寄存器指向 G 提供的函数,然后去执行。

P

P(Processor) 是一个抽象的概念,并不是真正的物理 CPU。所以当 P 有任务时需要创建或者唤醒一个系统线程来执行它队列里的任务。所以P/M需要进行绑定,构成一个执行单元。

P 决定了同时可以并发任务的数量,可通过 GOMAXPROCS 限制同时执行用户级任务的操作系统线程。可以通过 runtime.GOMAXPROCS 进行指定。在Go1.5之后 GOMAXPROCS 被默认设置可用的核数,而之前则默认为 1。

调度过程


首先创建一个 G 对象,G 对象保存到 P 本地队列或者是全局队列。P 此时去唤醒一个 M。P 继续执行它的执行序。M 寻找是否有空闲的 P,如果有则将该 G 对象移动到它本身。接下来 M 执行一个调度循环(调用 G 对象 -> 执行 -> 清理线程 -> 继续找新的 Goroutine 执行)。

M 执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go 调度器 M 的栈保存在 G 对象上,只需要将 M 所需要的寄存器(SP、PC等)保存到 G 对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时 G 任务还没有执行完,M 可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M 通过访问 G 的 vdsoSP、vdsoPC 寄存器进行现场恢复(从上次中断位置继续执行)。