Go 协程goroutine
外观
Go协程(Goroutine)是Go语言中实现并发编程的核心机制,它是一种轻量级的线程,由Go运行时(runtime)管理,能够在极小的栈空间(初始仅2KB)上运行,并支持动态扩容。与操作系统线程相比,协程的创建和切换成本极低,使得开发者可以轻松创建成千上万的并发任务。
基本概念[编辑 | 编辑源代码]
Goroutine是Go语言对协程(Coroutine)的实现,但相比传统协程,它进一步与Go的调度器深度集成,支持多路复用操作系统线程(通过M:N调度模型)。其核心特点包括:
- 轻量级:内存占用远小于线程(默认线程栈为1-8MB)。
- 低成本创建与切换:由Go运行时调度,不依赖操作系统内核。
- 通信通过Channel:遵循“不要通过共享内存来通信,而应通过通信来共享内存”的设计哲学。
与线程的对比[编辑 | 编辑源代码]
特性 | Goroutine | 系统线程 |
---|---|---|
创建成本 | 微秒级,栈可动态增长 | 毫秒级,固定栈大小 |
内存占用 | 初始2KB | 通常1-8MB |
调度方式 | Go运行时抢占式调度 | 操作系统内核调度 |
通信机制 | Channel(类型安全) | 共享内存(需锁) |
基本用法[编辑 | 编辑源代码]
通过go
关键字即可启动一个Goroutine,语法如下:
go 函数名(参数列表)
示例:并发执行[编辑 | 编辑源代码]
以下代码展示主线程与Goroutine的并发执行:
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 1; i <= 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func main() {
go printNumbers() // 启动Goroutine
fmt.Println("Main starts")
time.Sleep(500 * time.Millisecond) // 等待Goroutine完成
fmt.Println("\nMain ends")
}
输出:
Main starts 1 2 3 Main ends
关键点:
- 主线程退出时,所有Goroutine会被强制终止(此处通过
time.Sleep
等待)。 - Goroutine的执行顺序非确定性的(若移除
Sleep
,可能看不到输出)。
调度模型[编辑 | 编辑源代码]
Go使用M:N调度模型,其中:
- M:操作系统线程(由内核管理)
- N:Goroutine(用户态轻量线程)
- P:逻辑处理器(绑定线程的上下文)
调度器特点:
- 工作窃取(Work Stealing):空闲P会从其他P的队列中偷取Goroutine。
- 抢占式调度:防止单个Goroutine长时间占用CPU(Go 1.14+支持基于信号的抢占)。
同步与通信[编辑 | 编辑源代码]
Goroutine间通常通过channel
通信,但也可使用sync
包提供的锁机制。
使用Channel同步[编辑 | 编辑源代码]
func worker(done chan bool) {
fmt.Print("Working...")
time.Sleep(1 * time.Second)
done <- true // 发送完成信号
}
func main() {
done := make(chan bool)
go worker(done)
<-done // 阻塞直到收到信号
fmt.Println("Done")
}
使用WaitGroup[编辑 | 编辑源代码]
适用于等待一组Goroutine完成:
var wg sync.WaitGroup
func task(id int) {
defer wg.Done() // 等价于 wg.Add(-1)
fmt.Printf("Task %d done\n", id)
}
func main() {
for i := 1; i <= 3; i++ {
wg.Add(1)
go task(i)
}
wg.Wait() // 阻塞直到计数器归零
fmt.Println("All tasks completed")
}
实际应用场景[编辑 | 编辑源代码]
1. Web服务器并发处理请求[编辑 | 编辑源代码]
每个HTTP请求在一个独立的Goroutine中处理:
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Request from %s", r.RemoteAddr)
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil) // 每个连接启动一个Goroutine
}
2. 并行计算[编辑 | 编辑源代码]
利用多核CPU加速计算(示例:并行计算斐波那契数列):
func fib(n int) int {
if n <= 1 { return n }
return fib(n-1) + fib(n-2)
}
func main() {
const N = 40
results := make(chan int, 2)
go func() { results <- fib(N) }()
go func() { results <- fib(N-1) }()
total := <-results + <-results
fmt.Println("Sum:", total)
}
高级主题[编辑 | 编辑源代码]
Goroutine泄漏[编辑 | 编辑源代码]
未正确退出的Goroutine会导致内存泄漏。常见原因:
- 无缓冲Channel阻塞
- 无限循环无退出条件
检测工具:
runtime.NumGoroutine()
监控协程数量- 使用
pprof
进行性能分析
上下文(Context)控制[编辑 | 编辑源代码]
用于传递取消信号和超时控制:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go func(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("Work done")
case <-ctx.Done():
fmt.Println("Cancelled:", ctx.Err())
}
}(ctx)
性能考量[编辑 | 编辑源代码]
- GOMAXPROCS:控制使用的CPU核心数(默认等于逻辑CPU数)。
- 避免过度创建Goroutine(虽然轻量,但并非无成本)。
- 对于CPU密集型任务,需注意Amdahl定律的并行限制:
其中为并行比例,为处理器数量。
总结[编辑 | 编辑源代码]
Goroutine是Go并发模型的核心,其轻量级特性和与Channel的深度整合,使得编写高并发程序变得简单高效。开发者需注意:
- 优先使用Channel而非共享内存
- 通过
sync
或context
实现同步 - 监控并防止Goroutine泄漏
掌握Goroutine是成为高效Go开发者的关键一步!