Golang 协程(Goroutine)
一、基本概念
Go 协程(Goroutine)是 Go 语言并发模型的核心,是一种由运行时管理的轻量级线程。
进程、线程和协程的关系:
- 线程可以理解为轻量级的进程
- 协程可以理解为轻量级的线程
二、核心特性
1. 资源占用极低
-
创建成本低
相比线程,协程的创建和销毁成本更低,能支持大规模并发。 -
动态栈
栈空间按需扩展,避免固定栈导致的资源浪费。
2. 调度机制高效
-
GMP 模型
下文详述其具体实现。 -
工作窃取(Work-Stealing)
当 P 的本地队列为空时,会从其他 P 窃取 50% 的 G,实现负载均衡。 -
抢占式调度
Go 1.14+ 引入基于信号的异步抢占,避免单个 G 长时间占用 CPU。
三、创建与通信
1. 启动协程
在调用函数前加 go 关键字即可启动协程:
go task() // 启动协程执行 task 函数
(1)主协程的工作
封装 main() 函数的 goroutine 称为主协程,其他为子协程。主协程的工作流程:
- 首先设定每个协程可申请的栈空间最大尺寸(不同系统尺寸不同,超过会引发栈溢出恐慌,导致程序终止)。
- 然后进行初始化工作:
- 创建特殊的
defer语句,用于主协程退出时的善后处理(应对主协程非正常结束)。 - 启动后台内存垃圾清扫协程,并设置 GC 可用标识。
- 执行
main包中的init()函数。 - 执行
main()函数(执行完后检查是否引发运行时恐慌并处理)。
- 创建特殊的
- 最后,主协程结束自己及当前进程。
(2)协程规则
- 新 goroutine 启动时,调用立即返回,Go 不等待其执行结束,忽略返回值后立即执行下一行代码。
- 主协程退出会终止所有子协程。
(3)示例
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
// 循环打印消息
func show(msg string) {
for i := 0; i < 5; i++ {
fmt.Printf("msg: %v\n", msg)
time.Sleep(time.Millisecond * 100)
}
}
// 测试协程基本运行
func test01() {
go show("golang1") // 启动子协程
show("golang2") // 主协程执行
fmt.Println("end...")
}
// 发送 HTTP 请求并打印响应体长度
func responseSize(url string) {
fmt.Println("step1: ", url)
response, err := http.Get(url)
if err != nil {
log.Fatal(err)
}
fmt.Println("step2: ", url)
defer response.Body.Close()
fmt.Println("step3: ", url)
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Fatal(err)
}
fmt.Println("step4: ", len(body))
}
// 测试多协程并发请求
func test02() {
go responseSize("https://www.baidu.com")
go responseSize("https://www.hldx.com")
go responseSize("https://jd.com")
time.Sleep(time.Second * 10) // 等待子协程执行完成
}
func main() {
// test01()
test02()
}
2. 同步控制
(1)sync.WaitGroup
用于等待一组协程完成,避免主协程提前退出:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
fmt.Println(id)
wg.Done()
}(i)
}
wg.Wait()
}
(2)Context 取消
通过上下文传递终止信号,优雅结束协程(用于需要主动终止协程的场景)。
3. 协程间通信
(1)通道(Channel)
用于协程间数据传递,分为无缓冲和带缓冲两种:
- 无缓冲通道:同步阻塞,发送和接收操作需同时就绪。
- 带缓冲通道:异步非阻塞,仅当缓冲区满(发送)或空(接收)时才阻塞。
(2)Select 多路复用
监听多个通道,响应最先就绪的操作,常用于处理多通道数据。
4. 协程时间轴分析

- 可通过
time.Sleep控制程序执行过程,观察协程运行顺序。
四、GMP 模型
GMP 是 Go 的并发调度模型,实现了 M:N 线程模型(M 个用户线程(协程)运行在 N 个内核线程上)。
1. 线程模型对比
(1)N:1 模型
N 个用户线程运行在 1 个内核线程中。
- 优点:用户线程上下文切换快。
- 缺点:无法充分利用 CPU 多核算力。
(2)1:1 模型
每个用户线程对应 1 个内核线程。
- 优点:充分利用 CPU 算力。
- 缺点:线程上下文切换慢。
(3)M:N 模型(Go 实现)
- 优点:兼顾 CPU 算力利用和协程上下文切换效率。
- 缺点:调度算法较复杂。
2. 核心组件
- G(Goroutine)
Go 协程实例,存储执行状态(如程序计数器、栈信息等)。
- M(Machine)
操作系统线程,绑定 CPU 执行具体任务。
- P(Processor)
逻辑处理器,管理本地队列(LRQ)并调度 G 到 M 运行。其数量由 GOMAXPROCS 控制,默认等于 CPU 核数。
3. 调度器模型

- 每个操作系统线程 M 持有一个逻辑处理器 P。
- 每个 P 有一个本地队列(LRQ),还有一个全局队列(由多个 P 共享),队列中维护待执行的协程 G。
- 执行时,P 会将队列中的 G 调度到 M 中执行。
4. 调度策略
(1)队列轮转
- P 周期性将 G 调度到 M 执行,一段时间后保存上下文,将 G 放到队列尾部,再取新 G 调度。
- P 会周期性查看全局队列,调度其中的 G 到 M 执行。
(2)系统调用(阻塞处理)
- Go 提供 M 缓存池。
- 当 G0 即将进入系统调用时,M0 释放 P,空闲的 M1(来自缓存池或新建)获取 P 继续执行队列中剩余 G。
- G0 结束系统调用后:
- 若有空闲 P,M0 获取 P 继续执行 G0。
- 若无空闲 P,G0 放入全局队列等待调度,M0 进入缓存池沉睡。
(3)工作量窃取
- 新创建的 G 优先放入当前 P 的本地队列。
- 本地队列满时,转移 50% 的 G 到全局队列;本地队列为空时,先检查全局队列,若为空则从其他 P 偷取协程。
(4)抢占式调度
- 信号式抢占(Go 1.14+):监控线程
sysmon每 10ms 检测一次,强制抢占运行超 10ms 的 G。 - 目的:避免单个协程长时间占用 CPU,影响其他协程调度。