GMP并发模型
进程与线程与协程
多个线程属于同一个进程并共享内存空间,线程之间的通讯基于共享的内存进行。
Go语言的调度器使用与CPU数量相等的线程来调度多个Goroutine。
”为什么用Go语言?“
进程、线程存在问题:
- CPU高消耗
- 切换线程上下文需要申请、销毁资源消耗时间高
- 内存高占用
- 线程占用1M以上的内存空间
协程(Goroutine)的优点:
- 占用的内存更小(几kb)
- 初始为2kb,如果栈空间不足则自动扩容
- 调度更灵活(runtime调度)
- Go自己实现的调度器,创建和销毁的消耗非常小,是用户级。
- 抢占式调度(10ms)
- 编译器插入抢占指令,函数调用时检查当前Goroutine是否发起抢占请求
- 1.14版本后支持基于信号的异步抢占(20ms)
- 垃圾回收扫描栈时触发抢占调度
- 解决抢占式调度因垃圾回收和循环长时间占用资源(无法执行抢占指令)导致程序暂停
GMP并发模型
GMP
G 需要在 M 上才能运行,M 依赖 P 提供的资源,P 则持有待运行的 G,M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。
G: 取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值
M: 取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人
P:取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。
”你了解过GMP并发模型吗?“
GM老版调度器:
- 激烈的锁竞争
- 从全局队列中获取G,需要加锁
- 局部性差
- 比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。
- 系统开销大
- 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。
GMP新版调度器(记忆图-GMP)
-
解决GM老版调度器的问题
-
M(线程):N(协程)关系
-
创建 M 个线程(CPU 执行调度的单位),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行。
-
在同一时刻,一个线程上只能跑一个 goroutine。当 goroutine 发生阻塞时,runtime 会把当前 goroutine 调度走,让其他 goroutine 来执行。
-
-
任务偷取(work stealing)
- 全局队列已经没有 G,那 m 就要执行 work stealing (偷取):从其他有 G 的 P 哪里偷取一半 G 过来,放到自己的 P 本地队列
-
让出执行权(hand off)
- 某个G堵塞,线程释放绑定的P,把P转移给其它空闲线程
“go func() 执行过程?”
- go关键字创建一个goroutine入队,如果本地P队列满了则入队全局G队列
- 从P队列中队头的G交给M执行
- P有两个关键特性
- work stealing
- hand off