---
id: 208cfd8127284f07337b5f5002f3351d
author: hamori
title: Go 协程(Goroutine)的运行原理：2KB 可变栈与 M:N GMP 调度器
description: 从并发与并行的区别入手,阐述goroutine的2KB初始可变栈、用户级上下文切换、GOMAXPROCS,以及GMP(Goroutine-Machine-Processor)运行时调度模型。
language: zh
date: 2025-08-22T13:28:15.766056383Z
path: /blog/posts/goroutine-basics-z3fbe209e
category: concurrency
---

# Goroutine

提到Gopher们对golang优点的介绍时，经常出现的是与**并发性(Concurrency)**相关的内容。其基础是能够轻量且简便地处理任务的**goroutine**。在此，我简要地整理了相关内容。

### **并发性(Concurrency) vs 并行性(Parallelism)**

在理解goroutine之前，我想先梳理两个经常被混淆的概念。

- 并发性：并发性是关于同时处理大量任务的概念。它并不一定意味着实际上同时执行，而是一种结构性、逻辑性的概念，即将多个任务划分为小单位并交替执行，从而使用户看来多个任务仿佛同时在进行。即使在单核中，并发性也是可以实现的。
- 并行性：并行性是指“在多个核心上同时处理多个任务”。字面意义上，它是并行地推进工作，同时执行其他任务。

goroutine通过Go运行时调度器轻松实现并发性，并通过`GOMAXPROCS`设置自然地利用并行性。

通常使用率较高的Java多线程(Multi thread)是并行性的代表性概念。

# goroutine为什么好？

### 轻量(lightweight)

其创建成本与其他语言相比非常低。这里自然会产生疑问：为什么golang使用得较少呢？这是因为创建位置由Go运行时内部管理。原因在于它是上述的轻量级逻辑线程，比OS线程单位更小，初始栈大约需要2KB的大小，并且可根据用户的实现添加栈，从而实现动态可变。

由于以栈为单位进行管理，创建和销毁非常快速且成本低廉，因此即使运行数百万个goroutine也不会造成负担。由此，Goroutine借助运行时调度器，能够最大限度地减少对OS内核的介入。

### 性能良好(performance)

首先，正如上面所述，Goroutine对OS内核的介入较少，因此在用户级别(User-Level)进行上下文切换时，其成本比OS线程单位低廉，能够快速切换任务。

此外，它采用M:N模型，将goroutine分配到OS线程上进行管理。通过创建OS线程池，无需大量线程，仅用少量线程即可完成处理。例如，当发生系统调用等阻塞状态时，Go运行时会在该OS线程上执行其他goroutine，因此OS线程不会空闲，能够高效地利用CPU，实现快速处理。

由此，Golang尤其在I/O作业中能够比其他语言展现更高的性能。

### 简洁(concise)

需要并发处理时，仅用`go`关键字即可轻松处理函数，这也是一大优点。

使用`Mutex`、`Semaphore`等复杂的Lock时，必须考虑死锁(DeadLock)状态，因而在开发之前的设计阶段就不得不经历复杂的步骤。

Goroutine遵循“不要通过共享内存来通信，而要通过通信来共享内存”的理念，推荐通过`通道(Channel)`进行数据传递，`SELECT`还支持与通道(Channel)结合，使能够从已准备好数据的通道开始处理。此外，使用`sync.WaitGroup`可以简单地等待多个goroutine全部结束，从而轻松管理工作流程。得益于这些工具，可以防止线程间的数据竞争问题，更安全地进行并发处理。

另外，借助上下文(context)，可以在用户级别(User-Level)控制生命周期、取消、超时、截止时间和请求范围，因此能够在一定程度上保证稳定性。

# Goroutine的并行作业(GOMAXPROCS)

虽然已经介绍了goroutine在并发方面的优点，但您可能会疑惑：它是否不支持并行？这是因为近年来CPU的核心数量已不同于过去，达到两位数以上，家用PC也配备了相当多的核心。

然而，Goroutine同样进行并行作业，这就是`GOMAXPROCS`。

若不设置`GOMAXPROCS`，则会根据版本而有所不同。

1. 1.5之前：默认值为1，若需要大于1的值，必须通过类似`runtime.GOMAXPOCS(runtime.NumCPU())`的方式进行设置
2. 1.5 ~ 1.24：更改为使用所有可用的逻辑核心数。从此以后，除非存在较大限制的情况，开发者无需进行设置

3. 1.25：作为在容器环境中以善于适应著称的语言，它会检查linux上的cGroup，确认容器中设置的`CPU限制`。

   例如，若逻辑核心数为10个，而CPU限制值为5，则`GOMAXPROCS`会设置为较低的数值5。

1.25的修改具有非常重大的意义，因为语言在容器环境中的可用性得到了提升。由此，可以减少不必要的线程创建和上下文切换，从而防止CPU节流(throttling)。

```go
package main

import (
	"fmt"
	"math/rand"
	"runtime"
	"time"
)

func exe(name int, wg *sync.WaitGroup) {
	defer wg.Done()

	fmt.Printf("Goroutine %d: 시작\n", name) // 开始
	time.Sleep(10 * time.Millisecond) // 用于模拟作业的延迟
	fmt.Printf("Goroutine %d: 시작\n", name) // 开始
}

func main() {
	runtime.GOMAXPROCS(2) // 仅使用2个CPU核心
	wg := sync.WaitGroup();
  goroutineCount := 10
	wg.Add(goroutineCount)

	for i := 0; i < goroutineCount; i++ {
		go exe(i, &wg)
	}

	fmt.Println("正在等待所有goroutine结束...")
	wg.Wait()
	fmt.Println("所有作业已完成。")

}

```

# Goroutine的调度器(M:N模型)

关于前面提到的“采用M:N模型，将goroutine分配到OS线程上进行管理”这一点，若进一步具体说明，则存在goroutine GMP模型。

- G (Goroutine)：Go中执行的最小作业单位
- M (Machine)：OS线程（实际作业的位置）
- P (Processor)：由Go运行时管理的逻辑处理器

P额外拥有本地运行队列(Local Run Queue)，并承担将分配的G交给M的调度器角色。简而言之，goroutine的GMP运行过程如下。

1. 当G(Goroutine)被创建时，会将其分配到P(Processor)的本地运行队列中。
2. P(Processor)将本地运行队列中的G(Goroutine)分配给M(Machine)。
3. M(Machine)会返回G(Goroutine)的状态，即block、complete或preempted。
4. 工作窃取(Work-Stealing)：若P的本地运行队列为空，其他P会先检查全局队列。若那里也没有G(Goroutine)，则窃取其他本地P(Processor)的任务，使所有M都能不停地工作。
5. 系统调用处理(阻塞)：当G(Goroutine)在执行中发生阻塞时，M(Machine)会进入等待状态，此时P(Processor)会与被阻塞的M(Machine)分离，并与其他M(Machine)结合，执行下一个G(Goroutine)。因此在I/O作业等待期间也不会浪费CPU。
6. 当某个G(Goroutine)长时间占用(preempted)时，会将执行机会让给其他G(Goroutine)。

Golang的GC(Garbage Collector)同样在Goroutine之上运行，能够在将应用程序的暂停(STW)降至最低限度的同时并行地清理内存，从而高效利用系统资源。

最后，Golang是一门拥有强大优势的语言，此外还有许多其他优点，希望众多开发者能够享受使用Go语言的乐趣。

谢谢。
