1 背景

为了提高高并发下, CPU的利用率, Golang引入了GMP

(1) 进程 线程

对于 Linux 操作系统来讲,cpu 对进程的态度和线程的态度是一样的。
在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存 (进程虚拟内存会占用 4GB, 而线程也要大约 4MB)。
image.png

(2) 协程

其实一个线程分为 “内核态 “线程和” 用户态 “线程。
一个 “用户态线程” 必须要绑定一个 “内核态线程”,但是 CPU 并不知道有 “用户态线程” 的存在,它只知道它运行的是一个 “内核态线程”(Linux 的 PCB 进程控制块)。
image.png image.png
这样,我们再去细化去分类一下,

内核线程依然叫 “线程 (thread)”, 用户线程叫 “协程 (co-routine)”.

(3) 协程与线程的映射关系

1) N:1

image.png
N 个协程绑定 1 个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点,1 个进程的所有协程都绑定在 1 个线程上

缺点:
某个程序用不了硬件的多核加速能力
一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。

2) 1:1

image.png
协程的创建、删除和切换的代价都由 CPU 完成,有点略显昂贵了。

3) M:N

image.png
M 个协程绑定 1 个线程,是 N:1 和 1:1 类型的结合,克服了以上 2 种模型的缺点,但实现起来最为复杂。

2 GMP

G (goroutine)代表 用户线程
M (work thread) 代表 内核线程
P (processor) 代表 逻辑处理器
image.png

  • 所有的P在程序启动时就被自动创建
  • 一个G被创建时, 它会被优先放到P的本地队列中, 如果队列满了, 会把本地队列中的一半的G移到全局队列
  • M运行任务时会从P的本地队列中获取G, 当P本地队列为空时, M会尝试从全局队列中拿一批G放到P的本地队列, 或者从其它P的本地队列偷一半放到自己P的本地队列