什么是goroutine
goroutine
是由Go运行时管理的用户层轻量级线程。
相比较于操作系统线程,goroutine
的资源占用和使用代价都要小得多,可以创建几十个、几百个甚至成千上万个goroutine
也不会造成系统资源的枯竭,Go
的运行时负责对goroutine
进行管理。而所谓的管理 == 调度。
goroutine调度器
一个Go程序中可以创建成千上万个并发的goroutine
,而将这些goroutine
按照一定算法放到CPU上执行的程序就称为goroutine调度器。
一个Go程序对于操作系统来说只是一个用户层程序,操作系统眼中只有线程,goroutine
的调度全要靠Go自己完成。
goroutine调度器原理
G、P、M
- G:代表
goroutine
,存储了goroutine
的执行栈信息,goroutine
状态以及goroutine
的任务函数等。另外G对象是可以重用的。 - P:代表逻辑
processor
,P的数量决定了系统内最大可并行的G的数量。P中最有用的是其拥有的各种G对象队列、链表、一些缓存和状态。 - M:代表真正的执行计算资源。在绑定有效的P后,进入一个调度循环;而调度循环的机制大致是从各种队列、P的本地运行队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit做清理工作并回到M。如此返回。M不保留G的状态,这是G可以跨M调度的基础。
G被抢占调度
如果某个G没有进行系统调用、没有进行I/O操作、没有阻塞在一个channel的操作上,那么M是如何让G停下来并调度下一个可运行的G的呢?——G是被抢占调度的。
1 | // forcePreemptNS is the time slice given to a G before it is |
如果一个G任务运行超过10ms,sysmon
就会认为其运行时间太久而发出抢占式调度的请求。一旦G的抢占标志被设为true,那么在这个G下一次调用函数或方法时,运行时便可以将G抢占并移出运行状态,放入P的本地运行队列中,等待下一次被调度。
channel阻塞或网络I/O情况下的调度
如果G被阻塞在某个channel操作或网络I/O操作上,那么G会被放置到某个等待队列中,而M会尝试运行P的下一个可运行的G。如果此时P没有可运行的G供M运行,那么M将解绑P,并进入挂起状态。当I/O操作完成或channel操作完成,在等待队列中的G会被唤醒,标记为runnable-可运行,并被放入某个P的队列中,绑定一个M后继续执行。
系统调用阻塞情况下的调度
如果G被阻塞在某个系统调用上,那么不仅G会阻塞,执行该G的M也会解绑P,与G一起进入阻塞状态。
如果此时有空闲的M,则P会与其绑定并继续执行其他G;如果没有空闲的M,但仍然有其他G要执行,那么就会创建一个新M(线程)。
当系统调用返回后,阻塞在该系统调用上的G会尝试获取一个可用的P,如果有可用P,之前运行该G的M将绑定P继续运行G;如果没有可用的P,那么G与M之间的关联将解除,同时G会被标记为runnable,放入全局的运行队列中,等待调度器的再次调度。