go基础第四篇:GMP
首先明确一点,goroutine不是线程,线程是操作系统管理的,而goroutine其实是Go runtime管理的用户态协程。
一个go程序可以开几十万个goroutine,但操作系统只看到几个线程在跑。goroutine在用户态调度,线程在内核态调度,这就是goroutine为什么能这么轻量。
一般情况下,创建一个线程,操作系统要分配8MB的栈空间,还要在内核态记录一堆状态。创建一个goroutine,Go runtine只需分配2KB的栈空间,其他什么都不用干。这就是为什么开10万个线程机器直接炸,开10万个goroutine啥事没有。goroutine的栈大小不是固定的,初始2KB,用完了会自动扩容。扩的时候,Go runtime会分配一块新的、更大的栈空间(2倍大小),把旧栈的数据拷进去,然后继续跑。线程的栈是固定的,用不完也得占着。10万个线程就是800G,而10万个goroutine才200MB,完全可以接受。
GMP模型
goroutine的调度靠三个东西:G、M、P
G,即goroutine,就是写的那个go func()。
M,即Machine,是操作系统线程,真正在CPU上跑的东西。
P,即Processor,逻辑处理器,一个P绑定一个线程,并持有一个goroutine的本地队列,决定哪个goroutine在对应线程上跑。P的数量默认等于CPU核心数,可以通过runtime包的GOMAXPROCS函数调整。举个例子,一个直接运行在8核CPU机器上的go程序,默认有8个P,每个P绑定一个线程,该线程从P的goroutine本地队列里取goroutine来执行。值得注意的是,在Go 1.25之前,如果程序运行在容器中,runtime包的NumCPU函数获取的CPU数可能不准确,需要引入uber开源的automaxprocs包。从1.25开始,就不用引入了。
流程是,Go runtime发现go func()后,会创建一个goroutine,放到某个P的goroutine本地队列里(长度为256),本地队列放不下的话,会放到全局队列里。这个P绑定的线程,会持续从本地队列里拿出goroutine执行。如果这个goroutine本地队列的goroutine全都执行完了,则这个线程会去其他P的goroutine的本地队列里取goroutine执行,如果也拿不到,则会去全局队列里拿,这被称为work-stealing。
操作系统不知道goroutine,它只看到几个线程在跑。goroutine 的调度发生在 Go runtime 的用户态,不需要进入内核,因此上下文切换成本较低,通常只需要几百个 CPU 时钟周期。而线程切换需要进入内核,由操作系统调度,涉及内核态切换、调度器逻辑以及可能的缓存失效,因此通常需要几千甚至上万 CPU 时钟周期。
goroutine切换的时机
sleep
channel操作阻塞,比如从一个空的channel取数据,往一个满的channel放数据
系统调用。系统调用会阻塞线程,此时P会先和M解绑,再绑一个新的M,继续执行其他goroutine。旧M去执行系统调用,执行完后,会去尝试绑定原来的P,如果原来的P被别的M占了,则会去找空闲的P,如果没有,则M会休眠。
抢占。Go 1.14之后,加入了抢占式切换,长时间运行的goroutine会被强制切换。
但凡有goroutine panic了,如果没有recover,那么整个应用程序都会退出。
浙公网安备 33010602011771号