目录
[1. 协程](#1. 协程)
[1.1 概念](#1.1 概念)
[1.2 常规协程(Coroutine)](#1.2 常规协程(Coroutine))
[1.3 Go中的协程(Goroutine)](#1.3 Go中的协程(Goroutine))
[1.4 GMP模型](#1.4 GMP模型)
[1.4.1 获取和设置P的数量](#1.4.1 获取和设置P的数量)
[1.4.2. 协程的生命周期](#1.4.2. 协程的生命周期)
[1.4.3 协程的调度流程](#1.4.3 协程的调度流程)
[1.4.4 调度类型](#1.4.4 调度类型)
[2. 通道](#2. 通道)
[2.1 概念](#2.1 概念)
[2.2 发送与接收 channel](#2.2 发送与接收 channel)
[2.3 关闭 channel](#2.3 关闭 channel)
[2.4 遍历 channel](#2.4 遍历 channel)
[2.5 只读/只写channel](#2.5 只读/只写channel)
[2.6. select语句](#2.6. select语句)
1. 协程
1.1 概念
1. 进程(Process)
-
定义 :程序在操作系统中运行的实例,是 资源分配和调度的基本单位。
-
特点:
-
独立的内存空间(代码段、数据段、堆、栈)。
-
进程之间相互独立,数据共享需要 进程间通信(IPC),如管道、消息队列、共享内存。
-
创建/销毁/切换开销 最大(需要保存和恢复完整上下文)。
-
-
适用场景:任务之间相互独立,可靠性要求高,例如:浏览器主进程和渲染进程、数据库服务进程。
2. 线程(Thread)
-
定义:操作系统调度的最小执行单元,属于某个进程。
-
特点:
-
同一进程的线程共享 代码段、数据段、打开文件 等资源。
-
每个线程有自己的 栈空间和寄存器。
-
切换开销比进程小,但需要处理 同步 和 互斥(避免资源竞争)。
-
-
适用场景:计算密集型或 I/O 密集型的并行任务,如 Web 服务器中多线程处理请求。
3. 协程(Coroutine,Go 中的 Goroutine)
-
定义:用户态的轻量级线程,由语言运行时调度。
-
特点:
-
创建/销毁/切换成本非常低(比线程轻得多)。
-
通常运行在单个或少量线程中(M:N 调度模型)。
-
协作式调度(由语言运行时决定何时切换),而不是操作系统。
-
Go 里使用 Channel 实现安全通信。
-
-
适用场景 :高并发场景,如网络服务器(Go 的
net/http
)、聊天系统、游戏服务端。
4. 并发(Concurrency) vs 并行(Parallelism)
-
并发:
-
单核 CPU 通过 时间片切换 执行多个任务,看起来"同时"在跑。
-
强调 任务的交替推进。
-
举例:一个人同时和多个人聊天,快速在不同对话窗口切换。
-
-
并行:
-
多核 CPU 真正同时执行多个任务。
-
强调 任务的同时运行。
-
举例:多个人同时做不同的菜。
-
示例:
在Go中,通过在函数或方法的调用前加上go关键字即可创建一个go协程,并让其运行对应的函数或方法。
package main
import (
"fmt"
"time"
)
func printMsg(msg string) {
for i := 0; i < 3; i++ {
fmt.Println(msg, ":", i)
time.Sleep(time.Millisecond * 500)
}
}
func main() {
// 启动一个协程
go printMsg("goroutine 1")
// 主协程继续执行
printMsg("main")
// 等一会儿让子协程跑完
time.Sleep(time.Second * 3)
}
结果:
main : 0
goroutine 1 : 0
goroutine 1 : 1
main : 1
goroutine 1 : 2
main : 2
- 在Go中,当程序启动时会自动创建一个主协程来执行main函数,该协程与其他新创建的协程没有本质的区别,但主协程执行完毕后整个程序会退出,即使其他协程还未执行完毕,也会跟着退出。
- 如果一个协程在执行过程中触发了panic异常,但没有对其进行捕获,那么会导致整个程序崩溃,因此在协程中也需要通过recover函数对panic进行捕获。
1.2 常规协程(Coroutine)
协程是一种 用户态调度的轻量级执行单元,比线程更轻。
-
线程由操作系统内核调度;
-
协程则由 用户态的运行时库或框架 调度;
-
协程在本质上是 运行在线程里的函数,但它可以在执行过程中主动挂起、切换到其他协程。
特点:
并发而非并行
-
因为所有协程都绑定在同一个线程上,所以某一时刻只有一个协程在跑。
-
多个协程通过切换实现"看似同时",实际上仍是顺序执行。
-
所以它实现的是 并发 ,而不是 并行。
阻塞传递问题
-
如果某个协程调用了一个 阻塞的系统调用 (例如阻塞的
read
、sleep
等),那么它所依赖的线程就会挂起; -
由于线程被挂起,运行在该线程上的所有协程也都无法继续执行;
-
这就是"阻塞上升"的问题 → 一个协程阻塞,整个协程组阻塞。
1.3 Go中的协程(Goroutine)
Go语言中的协程(Goroutine)与常规的协程(Coroutine)的实现方式有所不同,Go中的协程不是与一个线程强绑定的,而是由Go调度器动态的将协程绑定到可用的线程上执行。
- 由于Go协程与线程之间的绑定是动态的,因此各个协程之间既能做到并发,也能做到并行。
- 当一个Go协程因为某些原因陷入阻塞,那么Go调度器会将该协程与其绑定的线程进行解绑,将线程的资源释放出来,使得线程可以与其他可调度的协程进行绑定。

1.4 GMP模型
GMP(Goroutine-Machine-Processor)模型是Go运行时系统中用于实现并发执行的模型,负责管理和调度协程的执行。G、M和P的含义分别如下:
流程图如下:

-
G(Goroutine):Go 的协程,包含自己的执行栈和状态;生命周期可跨 M,M 不记录 G 状态。
-
M(Machine):对应系统线程;需绑定 P 才能执行 G;调度 G 时由绑定的 P 指定。
-
P(Processor):调度器资源抽象;每个 P 有本地队列,管理可执行的 G;M 必须绑定 P 才能运行 G。
1. G 的存放队列
-
**本地队列(local queue):**每个 P 对应一个;接近无锁访问;调度优先级最高。
-
**全局队列(global queue):**所有 P 共享,用于存放本地队列满或调度不均衡的 G。
-
**wait 队列(wait queue):**存放 IO 阻塞后就绪的 G。
-
G 的创建与投递
-
新创建的 G 优先投递到当前 P 的本地队列。
-
本地队列满 → 投递到全局队列。
-
2. P 获取可调度 G 的流程
-
从当前 P 的本地队列获取 G。
-
从全局队列获取 G。
-
从 wait 队列获取 IO 就绪的 G。
-
从其他 P 的本地队列窃取一半 G(work-stealing),实现负载均衡。
-
每调度 61 次,P 会优先从全局队列获取一个 G,防止全局队列 G 饥饿。
说明:本地队列访问接近无锁,但因 work-stealing 存在部分锁操作。
1.4.1 获取和设置P的数量
-
GOMAXPROCS
表示 Go 运行时可以同时运行的 OS 线程数,即同时可执行的 M 的上限。 -
本质上,它限制了 并行执行的 P 数量,因为每个 M 必须绑定一个 P 才能运行 G。
package main
import (
"fmt"
"runtime"
)func main() {
cpuNum := runtime.NumCPU() // 获取本地机器的逻辑CPU数
fmt.Printf("cpuNum = %d\n", cpuNum) // cpuNum = 10runtime.GOMAXPROCS(6) // 设置可同时执行的最大CPU数 num := runtime.GOMAXPROCS(0) // 获取可同时执行的最大CPU数 fmt.Printf("num = %d\n", num) // num = 6
}
-
P 数量与 GOMAXPROCS 相关
-
Go 会创建与
GOMAXPROCS
相同数量的 P(Processor)。 -
每个 P 对应一个本地队列,用于调度 G。
-
-
M 的运行受 P 限制
-
M 线程必须绑定 P 才能运行 G。
-
同时运行的 M 数量不会超过
GOMAXPROCS
。
-
-
调度影响
-
增大
GOMAXPROCS
→ 可以利用更多 CPU 核心并行运行 G,提高吞吐量。 -
减小
GOMAXPROCS
→ 并行度降低,但可能减少上下文切换开销。
-
1.4.2. 协程的生命周期
状态 | 描述 |
---|---|
_Gidle | 协程刚创建,还未初始化。 |
_Grunnable | 协程已初始化并放入可运行队列,等待调度。 |
_Grunning | 协程正在被调度执行中。 |
_Gsyscall | 协程正在执行系统调用(阻塞可能发生)。 |
_Gwaiting | 协程处于等待状态,被挂起,需要特定事件或信号唤醒。 |
_Gdead | 协程已经执行完毕,生命周期结束。 |
流程图如下:

1.4.3 协程的调度流程
1. 协程类型
类型 | 描述 |
---|---|
普通 G | 用户通过 go 创建的协程,需要被调度执行。 |
g0 | 每个 M 都有一个 g0,是特殊的调度协程;负责调度普通 G 并管理 M 的执行。 |
monitor G | 全局监控协程,不通过 P,而直接绑定 M;负责监控各 P 的状态,触发抢占调度。 |
2. g0 的调度流程(每个 M 都有 g0)
-
**获取可调度的 G:**g0 通过 P 的本地队列、全局队列或 work-stealing 获取一个普通 G。
-
切换执行权
-
将 G 状态切换为
_Grunning
。 -
调用
gogo
函数,将执行权从 g0 交给 G。
-
-
**执行 G 的逻辑:**G 持续执行,直到被阻塞、IO 操作、channel 操作或者被抢占等条件触发调度结束。
-
调度结束
-
调用
mcall
函数,将执行权交还给 g0。 -
更新 G 的状态(如
_Grunnable
或_Gdead
)。
-
1.4.4调度类型
调度类型 | 描述 | 触发条件 |
---|---|---|
主动调度 | 当前 G 主动让出执行权,将自己投递到全局队列等待下一次调度。 | 用户调用 runtime.Gosched() 。 |
被动调度 | 当前 G 因阻塞而暂停执行。 | 等待锁、channel 条件、IO 阻塞等。 |
正常调度 | 当前 G 执行完自己的代码逻辑,调度结束。 | 代码逻辑执行完毕。 |
抢占调度 | monitor G 强制将当前 P 与 M 解绑,使 P 可以调度其他 G,M 继续执行系统调用。 | 满足抢占条件(如长期运行超过时间片)。 |
示意图如下:

2. 通道
2.1 概念
-
Channel 是 Go 提供的 类型安全的管道,用于在不同 goroutine 之间传递数据。
-
数据通过 channel 发送(send) 与 接收(receive),实现协程间同步和通信。
ch := make(chan int) // 创建一个整型无缓冲 channel
ch := make(chan int, 5) // 创建一个整型带缓冲 channel,缓冲区大小为5
**无缓冲 channel:**发送方必须等待接收方接收数据后才能继续,实现同步通信。
**带缓冲 channel:**发送方可以在缓冲区未满时直接发送数据,缓冲区满后才阻塞。
2.2 发送与接收 channel
channel的读写:
ch <- 10 // 发送数据到 channel
val := <-ch // 从 channel 接收数据
例如,下面程序中定义了一个容量为5的channel,并启动了一个协程不断向该channel中写入数据,而在主协程中每隔1秒从该channel中读取一次数据。如下:
package main
import (
"fmt"
"time"
)
func WriteNum(intChan chan int) {
num := 0
for {
intChan <- num // 向channel中写入数据
fmt.Printf("write a num: %d\n", num)
num++
}
}
func ReadNum(intChan chan int) {
for {
time.Sleep(time.Second)
num := <-intChan // 从channel中读取数据
fmt.Printf("read a num: %d\n", num)
}
}
func main() {
intChan := make(chan int, 5)
go WriteNum(intChan)
ReadNum(intChan)
}
2.3 关闭 channel
-
close(ch)
用于关闭 channel,关闭后不能再发送,但可以接收剩余数据。切可以从该channel中读取数据 -
接收者可以用两值形式判断 channel 是否关闭:
-
第一个值是从channel中读取到的数据,
-
第二个值表示本次对channel进行的读操作是否成功,
-
如果channel已关闭并且channel中没有数据可读,那么第二个值将会返回false,否则为true。
package main
import "fmt"
func main() {
charChan := make(chan int, 10)
for i := 0; i < 10; i++ {
charChan <- 'a' + i
}
close(charChan) // 关闭channelfor { ch, ok := <-charChan if !ok { fmt.Println("没有数据了") break } fmt.Printf("read a char: %c\n", ch) }
}
-
2.4 遍历 channel
遍历 channel 通常是用 for-range
循环来实现的,不过需要注意 channel 必须关闭,否则循环会永久阻塞。
-
在对channel进行读操作时,要确保有协程会对channel进行对应的写操作,否则会造成死锁(deadlock)。
-
如果去掉上述代码中关闭channel的操作,那么for range循环在读取完channel中的数据后不会自动结束迭代,而会继续进行读操作,但此时没有任何协程会再对该channel进行写操作,因此会造成死锁(deadlock)。
package main
import "fmt"
func main() {
ch := make(chan int, 5)// 启动一个 goroutine 发送数据 go func() { for i := 0; i < 5; i++ { ch <- i } close(ch) // 遍历 channel 前必须关闭 }() // 遍历 channel for v := range ch { fmt.Println(v) }
}
2.5 只读/只写channel
类型 | 描述 | 使用场景 |
---|---|---|
chan<- T |
只写 channel,只能发送 | 发送数据给其他 goroutine |
<-chan T |
只读 channel,只能接收 | 从其他 goroutine 接收数据 |
chan T |
可读可写 channel | 常规使用 |
package main
import "fmt"
// 函数内只能发送数据到 channel。
// 编译器会报错,如果尝试从 channel 接收。
func sendData(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// 函数内只能从 channel 接收数据。
// 编译器会报错,如果尝试向 channel 发送。
func receiveData(ch <-chan int) {
for v := range ch {
fmt.Println(v)
}
}
func main() {
ch := make(chan int)
go sendData(ch)
receiveData(ch)
}
2.6. select语句
-
select语句可以同时监听多个channel的操作,它会选择一个已经就绪的操作,并执行相应的分支代码。
-
如果有多个操作就绪,select语句会随机选择其中一个操作执行,如果没有操作就绪,则会执行default分支。
-
需要注意的是,如果没有操作就绪,并且select语句中没有default分支,则select语句会阻塞,直到至少有一个操作就绪。
select {
case v := <-ch1:
fmt.Println("从 ch1 接收:", v)
case ch2 <- 10:
fmt.Println("向 ch2 发送: 10")
default:
fmt.Println("没有 case 就绪,执行 default")
}
常见使用场景
(1)多路监听
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// 模拟两个数据源
go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自 ch1 的消息"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自 ch2 的消息"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("接收:", msg1)
case msg2 := <-ch2:
fmt.Println("接收:", msg2)
}
}
}
(2) 超时控制
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
go func() {
// 模拟耗时任务
time.Sleep(3 * time.Second)
ch <- 42
}()
select {
case v := <-ch:
fmt.Println("收到数据:", v)
case <-time.After(2 * time.Second): // 超时 2s
fmt.Println("等待超时,退出")
}
}
(3) 退出控制
package main
import (
"fmt"
"time"
)
func worker(quit chan struct{}) {
for {
select {
case <-quit: // 收到退出信号
fmt.Println("worker 收到退出信号,结束")
return
default:
fmt.Println("worker 工作中...")
time.Sleep(time.Second)
}
}
}
func main() {
quit := make(chan struct{})
go worker(quit)
time.Sleep(3 * time.Second) // 主 goroutine 等一会
close(quit) // 通知 worker 退出
time.Sleep(1 * time.Second) // 给 worker 一点收尾时间
}