goroutine 的状态
在 golang 中,我们使用go
创建一个新的gorotine,我们都知道操作系统线程有自己的状态, 比如 在 The time in computers: how long will it take to switch the context? 中,我们总结了线程的状态以及线程调度耗时。
goroutine也是一样的,有自己的状态,并且它的状态由 runtime 控制。
在 rimetime2.go 中定义了goroutine 的数据结构。g.atomicstatus 表示 goroutine 的状态。它的取值范围在源码中也有定义。
除了几个已经不被使用的以及与 GC 相关的状态之外,Goroutine 可能处于以下 9 种状态:
状态 | 描述 |
---|---|
_Gidle |
刚刚被分配并且还没有被初始化 |
_Grunnable |
没有执行代码,没有栈的所有权,存储在运行队列中 |
_Grunning |
可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P |
_Gsyscall |
正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程 M 但是不在运行队列上 |
_Gwaiting |
由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于 Channel 的等待队列上 |
_Gdead |
没有被使用,没有执行代码,可能有分配的栈 |
_Gcopystack |
栈正在被拷贝,没有执行代码,不在运行队列上 |
_Gpreempted |
由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒 |
_Gscan |
GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在 |
上述状态中比较常见是 _Grunnable 、_Grunning 、_Gsyscall 、_Gwaiting 和 _Gpreempted 五个状态,这里会重点介绍这几个状态。Goroutine 的状态迁移是个复杂的过程,触发 Goroutine 状态迁移的方法也很多,在这里我们也没有办法介绍全部的迁移路线,只会从中选择一些介绍。 |
虽然 Goroutine 在运行时中定义的状态非常多而且复杂,但是我们可以将这些不同的状态聚合成三种:等待中、可运行、运行中,运行期间会在这三种状态来回切换:
- 等待中:Goroutine 正在等待某些条件满足,例如:系统调用结束等,包括
_Gwaiting
、_Gsyscall
和_Gpreempted
几个状态; - 可运行:Goroutine 已经准备就绪,可以在线程运行,如果当前程序中有非常多的 Goroutine,每个 Goroutine 就可能会等待更多的时间,即
_Grunnable
;这个时候Goroutine 在 P的local queue 或者 全局队列中。 - 运行中:Goroutine 正在某个线程上运行,即
_Grunning
;
Grunnable
goroutine在下列几种情况会设置为Grunnable状态:
创建goroutine
在go中,包括用户入口main.mian在内的所有goroutine
都是通过runtime.newproc
->runtime.newproc1
创建的,前者是对后者的一层封装。go关键字最终会被编译器映射为对runtime.newproc
的调用。当runtime.newproc1完整资源分配及初始化后,新任务的状态会被置为Grunnable,然后被添加到当前P的本地任务队列中。
|
|
阻塞任务唤醒
当某个阻塞任务(Gwaiting)的等待条件满足而被唤醒时。(如g1项channel写入数据将唤醒等待接收的),g1通过调用runtime.ready将g2状态重新置为Grunnable并添加到任务队列中。关于groutine阻塞,还有更详细的介绍。
|
|
其他
另外的路径是从Grunning和Gsyscall状态转换到Grunnable,后面再介绍。总之处于Grunnable的任务一定是在某个任务队列中,随时等待被调度执行。
Grunning
所有状态为Grunnable
的任务都可能通过findrunnable
函数被调度器(P&M)获取,进而通过execute
将其状态切换到Grunning,最后调用runtime.gogo加载context并执行。
|
|
|
|
go采取的是一种协作式调度方案,一个正在运行的任务,需要通过yield的方式显式的让出处理器。
在Go1.2之后,runtime也支持一定程度的任务抢占–当系统线程sysmon发现某个任务执行时间过长或者runtime判断需要进行垃圾收集时,会将任务置为“可被抢占”的,当该任务下一次函数调用时,就会让出处理器并重新切换到Grunnable状态。
Gsyscall
Go运行时为了保证高的并发性能,当会在任务执行OS系统调用前,先调用runtime.entersyscall函数将自己的状态置为Gsyscall(如果系统调用是阻塞式的或者执行过久,则将当前M与P分离),当系统调用返回后,执行线程调用runtime.exitsyscall尝试重新获取P,如果成功且当前任务没有被抢占,则将状态切换回Grunning并继续执行;否则将状态置为Grunnable,等待再次被调度执行。
|
|
|
|
Gwaiting
当一个任务需要的资源或运行条件不能被满足时,需要调用runtime.park函数进入该状态,之后除非等待条件满足,否则任务将一直处于等待状态不能执行。除了之前举过的channel的例子外,Go的定时器,网络io操作,原子,信号量都可能引起任务的阻塞。
|
|
|
|
runtime.park中lock是goroutine阻塞时需要释放的锁,(比如channel),reason是阻塞的原因,方便gdb调试。当所有任务处于Gwaiting状态时,也就表示当前程序进入了死锁状态,那么runtime回检测到这种情况,并输出所有Gwaiting任务的backtrace信息。
Gdead
当一个任务执行结束后,会调用runtime.goexit结束。将状态置为Gdead,并进入当前P的gFree列表。
总结
goroutine的状态切换跟线程状态切换其实差不多,不过,因为gc的原因导致看上去复杂一点。但是如果去掉gc部分,其实goroutine 状态的切换跟 线程状态切换差不多。
下一篇文章,我将会总结分析一下 P的状态切换。