> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# goroutine 的状态 切换
- URL: https://huizhou92.com/goroutine-de-zhuang-tai-qie-huan/
- Published: 2025-01-06T02:35:19.000Z
- Updated: 2026-09-06T11:39:06.000Z
- Description: goroutine 的状态 切换. Goroutine 的状态 在 golang 中，我们使用 go 创建一个新的gorotine,我们都知道操作系统线程有自己的状态， 比如 在 The time in computers: how long will it take to switch the contex。
- Author: huizhou92
- Tags: tech, golang, #lang-zh-cn

### Goroutine 的状态

在 golang 中，我们使用`go` 创建一个新的gorotine,我们都知道操作系统线程有自己的状态， 比如 在 [The time in computers: how long will it take to switch the context?](https://re-learning-computer-journey.hxzhouh.com/time-in-computers-context-switching-edd5f957ec4e?ref=huizhou92.com) 中，我们总结了线程的状态以及线程调度耗时。  
![process status change,Uploaded by the author ](https://images.hxzhouh.com/blog-images/2024/04/576a746aeff4a30fd9abad0cb82bf74b.png)  
goroutine也是一样的，有自己的状态，并且它的状态由 runtime 控制。

在 [rimetime2.go](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/runtime2.go?ref=huizhou92.com#L414) 中定义了goroutine 的数据结构。[g.atomicstatus](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/runtime2.go?ref=huizhou92.com#L445) 表示 goroutine 的状态。它的[取值范围](https://github.com/golang/go/blob/ed817f1c4055a559a94afffecbb91c78e4f39942/src/runtime/runtime2.go?ref=huizhou92.com#L17)在源码中也有定义。  
除了几个已经不被使用的以及与 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`；  
![Pasted image 20240410114951](https://images.hxzhouh.com/blog-images/2024/04/576a746aeff4a30fd9abad0cb82bf74b.png)

### Grunnable

goroutine在下列几种情况会设置为Grunnable状态：

### 创建goroutine

在go中，包括用户入口main.mian在内的所有`goroutine`都是通过`runtime.newproc`\->`runtime.newproc1`创建的，前者是对后者的一层封装。go关键字最终会被编译器映射为对`runtime.newproc`的调用。当runtime.newproc1完整资源分配及初始化后，新任务的状态会被置为Grunnable，然后被添加到当前P的本地任务队列中。

```go
func newproc1(fn *funcval, argp unsafe.Pointer, narg int32, callergp *g, callerpc uintptr) {
  // --snip--
  // 获取当前g所在的p，从p中创建一个新g(newg)
  _p_ := _g_.m.p.ptr()
  newg := gfget(_p_)
  // --snip--
  // 设置Goroutine状态为Grunnable
  casgstatus(newg, _Gdead, _Grunnable)
  // --snip--
  // // 新创建的g添加到run队列中
  runqput(_p_, newg, true)
  // --snip--
}

```

### 阻塞任务唤醒

当某个阻塞任务(Gwaiting)的等待条件满足而被唤醒时。（如g1项channel写入数据将唤醒等待接收的)，g1通过调用runtime.ready将g2状态重新置为Grunnable并添加到任务队列中。关于groutine阻塞，还有更详细的介绍。

```go
func ready(gp *g, traceskip int, next bool) {
  // --snip--
  // 获取current g
  _g_ := getg()
  // 将状态从Gwaiting转换至Grunnable
  casgstatus(gp, _Gwaiting, _Grunnable)
  // 添加到运行队列中
  runqput(_g_.m.p.ptr(), gp, next)
  // --snip--
}

```

### 其他

另外的路径是从Grunning和Gsyscall状态转换到Grunnable，后面再介绍。总之处于Grunnable的任务一定是在某个任务队列中，随时等待被调度执行。

### Grunning

所有状态为`Grunnable`的任务都可能通过`findrunnable`函数被调度器(P&M)获取，进而通过`execute`将其状态切换到Grunning，最后调用runtime.gogo加载context并执行。

```go
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
  // --snip--
  // 挑一个可运行的g，并执行
  if gp == nil {
    gp, inheritTime = findrunnable() // blocks until work is available
  }
  // --snip--
  execute(gp, inheritTime)
}

```

```go
// Schedules gp to run on the current M.
func execute(gp *g, inheritTime bool) {
  // 将当前g的M切换到新的g上
  _g_ := getg()
  _g_.m.curg = gp
  gp.m = _g_.m
  // 将Grunnable状态变更为Grunning
  casgstatus(gp, _Grunnable, _Grunning)
  // --snip--
  // 真正执行goroutine
  gogo(&gp.sched)
}

```

go采取的是一种协作式调度方案，一个正在运行的任务，需要通过yield的方式显式的让出处理器。

在Go1.2之后，runtime也支持一定程度的任务抢占–当系统线程sysmon发现某个任务执行时间过长或者runtime判断需要进行垃圾收集时，会将任务置为“可被抢占”的，当该任务下一次函数调用时，就会让出处理器并重新切换到Grunnable状态。

### Gsyscall

Go运行时为了保证高的并发性能，当会在任务执行OS系统调用前，先调用runtime.entersyscall函数将自己的状态置为Gsyscall(如果系统调用是阻塞式的或者执行过久，则将当前M与P分离)，当系统调用返回后，执行线程调用runtime.exitsyscall尝试重新获取P，如果成功且当前任务没有被抢占，则将状态切换回Grunning并继续执行；否则将状态置为Grunnable，等待再次被调度执行。

```go
func reentersyscall(pc, sp uintptr) {
	_g_ := getg()
	// --snip--
	casgstatus(_g_, _Grunning, _Gsyscall)
	// --snip--
  // 将m和p分离
	pp := _g_.m.p.ptr()
	pp.m = 0
	_g_.m.oldp.set(pp)
	_g_.m.p = 0
	// --snip--
}

```

```go
func exitsyscall() {
  _g_ := getg()
  // --snip--
  // 如果P还存在则重新获取P
  if exitsyscallfast(oldp) {
    // --snip--
    casgstatus(_g_, _Gsyscall, _Grunning)
    // --snip--
    return
  }
  // --snip--
  // 如果P不存在则Gsyscall->Grunnable
  mcall(exitsyscall0)
  // --snip--
}

```

### Gwaiting

当一个任务需要的资源或运行条件不能被满足时，需要调用runtime.park函数进入该状态，之后除非等待条件满足，否则任务将一直处于等待状态不能执行。除了之前举过的channel的例子外，Go的定时器，网络io操作，原子，信号量都可能引起任务的阻塞。

```go
// park continuation on g0.
func park_m(gp *g) {
  // --snip--
  casgstatus(gp, _Grunning, _Gwaiting)
	// --snip--
}

```

```go
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
	// --snip--
  mcall(park_m)
}

```

runtime.park中lock是goroutine阻塞时需要释放的锁，(比如channel)，reason是阻塞的原因，方便gdb调试。当所有任务处于Gwaiting状态时，也就表示当前程序进入了死锁状态，那么runtime回检测到这种情况，并输出所有Gwaiting任务的backtrace信息。

### Gdead

当一个任务执行结束后，会调用runtime.goexit结束。将状态置为Gdead，并进入当前P的gFree列表。

### 总结

goroutine的状态切换跟线程状态切换其实差不多，不过，因为gc的原因导致看上去复杂一点。但是如果去掉gc部分，其实goroutine 状态的切换跟 线程状态切换差不多。  
下一篇文章，我将会总结分析一下 P的状态切换。