go的GMP

进程&线程&协程

进程

  • os中的 程序运行的实例。资源分配的基本单位。
  • 进程占用内存空间

线程

  • CPU调度的基本单位

  • 每个进程可以拥有多个线程

  • 线程占用CPU的时间

  • 线程使用系统分配给进程的内存,线程之间共享内存

线程的问题

  • 线程本身占用资源大
  • 线程切换开销大
  • 线程的操作开销大

协程将一段程序的运行状态打包,可以在线程之间调度

协程的优点

  • 资源利用
  • 快速调度
  • 超高并发

总结

  • 进程用来分配内存空间
  • 线程用来里分配CPU时间
  • 协程用来精细化利用线程(协程复用线程

协程的本质

  • runtime中,协程的本质是 一个 g结构体
  • stack:堆栈地址 (lo、hi)
  • gobuf 目前程序运行现场
  • atomicstatus:协程的状态

image-20240224151108486

线程的抽象

  • runtime中将 操作系统的线程抽象为 m 结构体
  • g0:go协程,操作调度器
  • curg: current g,目前 线程运行的g
  • mOS:操作系统线程的信息

协程是如何执行的

image-20240224153941032

schedule->execute->gogo()汇编实现->业务方法->goexit->g0的 schedule->… 循环往复

image-20240224155223636

  • 操作系统并不知道Goroutine的存在
  • 操作系统线程执行一个调度循环,顺序执行Goroutine

问题:

  • 顺序执行,无法并发,此时会引发协程的饥饿问题

  • 多线程并发时,会发生抢夺协程队列的全局锁的情况(使用本地队列)

单线程模型

多线程模型

G-M-P调度模型

GMP调度模型用来解决全局锁的争抢问题&顺序执行引发的饥饿问题

全局锁的争抢问题的解决

p结构体 (processor 送料器)
  • M与G之间的中介
  • p持有一些G,使得每次获取G的时候不用从全局中找
  • 大大减少了并发冲突问题

image-20240224160538300

本地全局都没有G,任务窃取,以增强线程的利用率

新建协程

  • 随机寻找一个p
  • 将新协程放入 P的runnext(插队)
  • 若本地队列都满了,放入全局队列

顺序执行引发的饥饿问题的解决

  • 如果协程顺序执行,会有饥饿问题

  • 协程执行中间,将协程挂起,执行其他协程

  • 完成系统调用时挂起,也可也主动(channel 锁。。)挂起

  • 防止全局队列饥饿,本地队列随机抽取全局队列 (每执行goroutine 61 次,从全局队列中取)

问题:

  • 永远不主动挂起
  • 永远不系统调用

此时还是会造成协程的饥饿

总结

  • 基于系统调用和主动挂起,协程可能无法调度
  • 基于协作的抢占式调度:业务主动调用 morestack()
  • 基于信号的抢占式调度:强制线程调用 doSigPreempt()

协程太多怎么办

  • 文件打开数限制
  • 内存限制
  • 调度开销过大

优化业务逻辑

利用 channel

  • 利用 channel 的缓存机制
  • 启动协程前,向channel 发送一个空结构体
  • 协程结束,去除一个空结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func do(i int, ch chan struct{}) {
fmt.Println(i)
time.Sleep(time.Second)
<-ch
}

func main() {
ch := make(chan struct{}, 3000)
for i := 0; i < math.MaxInt; i++ {
ch <- struct{}{}
go do(i, ch)
}
select {}
}

使用协程池

image-20240224174559462

总结

进程、线程、协程

协程的本质:

  • runtime的角度
  • 线程的角度

单线程模型

多线程模型

G-M-P模型

并发问题

饥饿问题

协程太多怎么优化


go的GMP
http://example.com/2024/02/24/go的GMP/
作者
Forrest
发布于
2024年2月24日
许可协议