Go 语言中的 Goroutine 详解
作为 Go 语言并发编程的核心,goroutine 是一种轻量级的“线程”,由 Go 运行时管理。它可以让你轻松地编写并发程序,而无需关心底层操作系统的线程调度。下面我们分几个方面详细讲解 goroutine 的概念、创建与调度,以及与并发、channel 的关系。
一、goroutine 的概念
1. 什么是 goroutine?
goroutine 是 Go 语言中实现并发的轻量级执行单元。你可以把它理解为一个独立执行的函数,但它比操作系统线程(OS thread)要小得多(初始栈大小仅 2KB,且可动态增长)。成千上万个 goroutine 可以同时运行在少量 OS 线程上,这使得 Go 程序能够轻松处理高并发任务。
2. goroutine 的特点
- 轻量:创建成本低,栈空间小且可伸缩。
- 并发执行:多个 goroutine 可以同时运行(如果是多核 CPU,可实现真正的并行)。
- 非阻塞:goroutine 通过 channel 进行通信,而不是共享内存,避免了传统多线程编程中的锁竞争。
- 由 Go 运行时调度:用户不需要直接干预线程的创建和销毁。
3. goroutine 与 OS 线程的对比
| 特性 | goroutine | OS 线程 |
|---|---|---|
| 创建成本 | 极低(几 KB 栈) | 高(通常 MB 级栈) |
| 切换成本 | 低(用户态调度) | 高(内核态调度) |
| 调度器 | Go 运行时调度器(GMP 模型) | 操作系统调度器 |
| 数量 | 可创建数十万甚至百万 | 受系统资源限制,通常几千 |
二、goroutine 的创建与调度
1. 创建 goroutine
只需在函数调用前加上关键字 go,该函数就会在一个新的 goroutine 中并发执行。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") // 启动一个新的 goroutine
say("hello") // 当前 goroutine 继续执行
}
go say("world")启动了一个新的 goroutine 并发执行say函数。- 主函数
main本身也运行在一个 goroutine 中(称为主 goroutine)。 - 当主 goroutine 结束时,程序会退出,而不管其他 goroutine 是否执行完毕。因此通常需要同步机制(如
WaitGroup或 channel)来等待其他 goroutine 完成。
2. goroutine 的调度模型:GMP
Go 运行时实现了一个用户态的调度器,采用 GMP 模型:
- G(Goroutine):代表一个 goroutine,包含栈、程序计数器等信息。
- M(Machine):代表 OS 线程(内核线程),是实际执行计算的资源。
- P(Processor):代表调度的上下文,它维护一个本地 goroutine 队列。P 的数量由环境变量
GOMAXPROCS控制(默认为 CPU 核心数)。
调度流程简述:
- 每个 P 负责调度一组 goroutine 到与之绑定的 M 上执行。
- 当 M 上运行的 goroutine 被阻塞(如系统调用、channel 操作)时,M 会释放 P,P 可以绑定到其他 M 继续执行剩余的 goroutine。
- 如果某个 P 的本地队列为空,它会从其他 P 或全局队列中偷取(work stealing)goroutine 来执行,以实现负载均衡。
这种设计使得 Go 程序能够高效地利用多核 CPU,同时保持 goroutine 创建和切换的低开销。
三、并发、协程与 channel
1. 并发与并行
- 并发(Concurrency):指程序结构上同时处理多个任务的能力(逻辑上的同时)。在 Go 中,通过 goroutine 和 channel 可以轻松构建并发程序。
- 并行(Parallelism):指物理上在同一时刻执行多个任务(需要多核 CPU)。Go 的调度器可以将多个 goroutine 分配到多个 OS 线程上,从而实现并行。
并发是结构,并行是执行。Go 鼓励通过并发组合来解决问题,而不必担心并行细节。
2. 协程(Coroutine)与 goroutine
“协程”是一个更广义的概念,指用户态轻量级线程,可以主动让出(yield)控制权。goroutine 是 Go 对协程的实现,但有一些不同:
- goroutine 是多核并发的,可以并行运行在不同线程上。
- goroutine 的调度是抢占式的(当 goroutine 阻塞或长时间运行时,调度器会强制切换),而不是完全由用户主动 yield。
- goroutine 之间通过 channel 通信,遵循“不要通过共享内存来通信,而应通过通信来共享内存”的理念。
3. channel:goroutine 之间的通信管道
channel 是 Go 语言提供的一种类型,用于在 goroutine 之间安全地传递数据。它类似于一个队列(FIFO),并内置了同步机制。
channel 的创建:
ch := make(chan int) // 无缓冲 channel
ch := make(chan string, 10) // 有缓冲 channel,缓冲区大小为 10
发送和接收:
ch <- value // 发送 value 到 channel
value := <-ch // 从 channel 接收值
close(ch) // 关闭 channel(接收方可以检测到关闭)
无缓冲 channel:发送和接收操作会阻塞,直到另一端准备好,从而实现同步。 有缓冲 channel:发送仅在缓冲区满时阻塞,接收仅在缓冲区空时阻塞。
示例:使用 channel 同步 goroutine
package main
import "fmt"
func worker(done chan bool) {
fmt.Println("working...")
// 模拟任务
done <- true // 发送完成信号
}
func main() {
done := make(chan bool)
go worker(done)
<-done // 等待接收,阻塞直到 worker 发送完成
fmt.Println("done")
}
示例:使用 channel 在多个 goroutine 间传递数据
package main
import "fmt"
func main() {
ch := make(chan int)
// 启动一个 goroutine 发送数据
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
// 主 goroutine 接收数据
for v := range ch {
fmt.Println(v)
}
}
当 channel 被关闭且缓冲区为空时,
range循环会自动退出。
4. select 多路复用
select 语句可以让一个 goroutine 同时等待多个 channel 操作,实现超时、非阻塞等机制。
select {
case msg1 := <-ch1:
fmt.Println("received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("received from ch2:", msg2)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no activity")
}
四、总结
- goroutine 是 Go 并发编程的基本单位,轻量且高效。
- 通过
go关键字轻松创建,由 Go 运行时调度(GMP 模型)。 - 并发是程序结构,并行是执行;goroutine 可以并行运行在多核上。
- channel 是 goroutine 之间通信的首选方式,遵循 CSP 模型(Communicating Sequential Processes)。
- 使用
select可以处理多个 channel 操作,实现复杂的并发控制。
给初学者的建议:
- 尝试用 goroutine 编写简单的并发程序,如并发下载、并发计算。
- 理解 channel 的阻塞特性,学会使用无缓冲 channel 同步,有缓冲 channel 解耦。
- 注意避免死锁(例如 goroutine 互相等待对方发送/接收)。
- 使用
sync.WaitGroup等工具等待多个 goroutine 完成,或者用 channel 本身进行同步。