GMP 是 Go 语言特有的协程调度模型:

其中 G 代表 Goroutine(协程),

M 代表 Machine(工作线程),

P 代表 Processor(逻辑处理器),

其中G、P 在用户空间,M在内核空间。

Goroutine(协程)本质上是一个go程序的函数,在主函数中用go关键字标注,那么这个函数就会独立于主函数并发并发运行,也就是说它可以不按程序的顺序执行,而是他会被Go runtime中的P(处理器)管理,由P决定它应该交给哪个工作线程运行。特别注意!!!go程序中所有的函数都会被当做协程对象,Go 程序中,所有代码最终都会通过协程(G)执行—— 即使是 main 函数这样的 “主逻辑”,本质上也是作为一个特殊的协程(主协程)运行的。

Processor(逻辑处理器)本质上是用户态的一个数据结构,其中维护了一个存储Goroutine的队列。 Go runtime 在用户态维护了一张 “M-P 绑定表”。当 M 需要运行 G 时,会向 Go runtime 请求绑定一个空闲的 P,绑定后 M 就能访问该 P 管理的本地 G 队列。P 的数量通常与 CPU 核心数相关(可通过 GOMAXPROCS 设置),一般取等,尽可能让在不同cpu上的M绑定p,充分利用cpu资源,除了每个 P 的本地队列,还有一个全局队列,当本地队列空时,M 会从其他 P 的队列或全局队列偷取 G 来执行(工作窃取机制)。

Machine(工作线程)位于内核态,在go程序开始运行时,由Go runtime创建,程序结束他会被系统回收其创建时的资源,当 M 执行 G 的代码时,M 会加载 G 的栈指针、程序计数器等上下文,会从内核态切换到用户态,当需要系统调用时,再从用户态切回内核态。

小提示:系统调用是用户态程序通过内核提供的接口,让内核完成它没权限做的事;而操作系统是 “内核 + 上层工具” 的整体,内核是其中负责处理系统调用、管理硬件的核心。

Go 语言的设计理念是 “一切皆协程”,如果在函数内部函数的并发运行,离不开工作线程,工作线程通过p获得g,因为g的切换完全在用户空间上的,加上g切换的资源开销非常小,使得在一个工作线程中可以执行多个协程,而无需自己切换,所以子协程即使崩了,也不会影响父协程的运行。这当然也有隐患,所以需要考虑并发问题和子协程和父协程的通信问题。

介绍了协程,这不得不再知识衍生到线程和进程:

进程是操作系统进行资源分配(如内存、文件描述符)的基本单位,值得注意的是,即使程序暂时未被 CPU 调度(处于就绪 / 阻塞态),只要已加载到内存并拥有独立资源空间,仍属于进程,进程的数据、代码、文件描述符、

虚拟内存空间是进程级独有资源(后续线程会共享),而寄存器、堆栈实际是线程运行时的私有资源。

线程的创建依赖于进程,线程切换快,核心是因为无需切换进程的地址空间(虚拟内存映射表) —— 进程切换需让 CPU 重新加载地址空间,而线程切换仅需保存 / 恢复自身的寄存器和栈上下文,操作成本远低于进程。因此说他是cpu调度的基本单位,

总之:操作系统通过进程划分资源边界(避免资源冲突),通过线程实现 CPU 的高效调度(减少资源切换开销)