1、什么是M、P、G,它们分别代表什么,为什么 Go 会选择 M:N 模型,而不是 1:1 ?
M:是操作系统线程,是Machine的缩写,一个M代表一个内核线程,也称工作线程(真正干活的);
G:是Go语言的轻量级线程,是Goroutine的缩写,一个G代表一个Go代码片段;
P:是Processor的缩写,负责G的调度,一个P代表执行一个Go代码片段所必须的资源;
如果选择 1:1 模型,意味着每个Goroutine都需要创建一个OS线程,这样的话不仅内存开销大,而且系统线程切换的成本也很高,不适合高并发场景;Go运行时是自己管理调度Goroutine,是用户级线程,可在用户动态实现高效的任务切换,从而使得调度更轻量,性能更好。
Go采用协作式调度 + 抢占式调度,这样可以减少OS系统线程切换的成本,P维护本地Goroutine队列,减少全局锁竞争,从而提高了调度效率。
M:N模型即保证了高并发性能,又避免了1:1模型的高成本,这样的设计可以让Go高效的管理成千上万的Goroutine,而不会过度依赖OS线程。
2、为什么 Go 要引入 P,直接用 M 和 G 不行吗?
如果没有P,M直接操作G,那Goroutine调度就会依赖OS系统的线程管理,会有高昂的线程切换开销。而P作为调度器,维护本地Goroutine队列,可以大大减少OS线程的调度开销,提高了性能。
3、Goroutine是如何被调度到M上执行的?
P维护着一个本地的Goroutine队列,M通过绑定P获取G来执行。当P的任务队列空了,它会从全局队列或其它P那里“偷取”任务,这样可以保证负载均衡。
4、P的数量可以动态调整吗?
P的数量由GOMAXPROCS决定,默认等于CPU核心数,但是可以用runtime.GOMAXPROCS(n)进行动态调整。在高并发情况下,调整P数量可能会影响性能。
5、Go什么时候会创建新的M ?
有Goroutine需要执行但没有空闲M时,Go运行时会创建新的M。
6、使用go关键字生成的goroutine是放置在P中,还是M中?
当一个G被创建并初始化完成后会立即被存储到本地P的runnext字段中,因为G必须先被添加入到P的可以运行G队列中才能在M中运行。
7、Goroutine调度有哪些优化点?
可以使用GOMAXPROCS调整P数量以适配不同的CPU负载;可以让Goroutine自己控制阻塞以减少调度器的上下文切换;可以减少Goroutine创建和销毁的开销(比如用sync.Pool复用)。
8、如何优化Go的高并发任务执行?
控制Goroutine数量,避免因创建过多Goroutine而导致运行时的调度开销增加;适当使用sync.WaitGroup或channel来管理任务的执行顺序;避免过度依赖全局变量,减少锁竞争。