Goroutine是什么?
Goroutine是Go的轻量级线程,由Go运行时管理。
和线程的区别是什么?
Goroutine 是 Go 语言实现并发的核心,它是在操作系统线程之上的“轻量级线程”。
1. 内存占用 (Memory)
- Goroutine:非常轻量。启动一个 Goroutine 只需要约 2 KB 的栈内存。且它的栈空间是动态的,可以根据需要自动增长或收缩,最大1G。
- 线程:相对较重。通常一个系统线程需要预分配 1 MB - 8 MB 的固定栈内存。
2. 创建与切换开销 (Context Switch)
- Goroutine:在用户态完成切换。由 Go 运行时(Runtime)调度,不需要进入操作系统内核。切换时只需保存极少数寄存器,耗时极短(约 200 纳秒)。
- 线程:在内核态完成切换。需要操作系统进行上下文切换,涉及大量的寄存器、内存页表等状态的保存与恢复,耗时较长(约 1-10 微秒)。
3. 调度模型 (Scheduling)
- Goroutine:采用 M:N 调度模型。Go 运行时将 M个 Goroutine 映射到 N个内核线程上。这意味着一个程序可以轻松运行数百万个 Goroutine,而不会耗尽系统资源。
- 线程:通常是 1:1 映射。一个用户线程对应一个内核线程,受系统资源限制,线程数量增加到一定程度后,性能会因频繁切换而剧烈下降。
4. 生命周期管理
- Goroutine:没有 ID 概念(不能像线程那样获取 ID 并操作),生命周期完全由 Go 运行时管理。
- 线程:有明确的 ID,操作系统提供丰富的 API 供用户手动控制线程的优先级、终止等。
总结对比表
| 特性 | Goroutine | 操作系统线程 |
|---|---|---|
| 内存占用 | 极小 (约 2KB) | 较大 (1MB+) |
| 栈空间 | 动态扩容/缩容 | 固定大小 |
| 调度方式 | 用户态 (Go Runtime) | 内核态 (OS Kernel) |
| 切换成本 | 极低 (约 200ns) | 较高 (约 1-10μs) |
| 并发量 | 数百万级别 | 数千至一万级别 |
创建Goroutine
// 普通调用
func Hello() {
fmt.Print("hello")
}
func main() {
Hello()
//Goroutine
go Hello()
//匿名函数
go func() {
fmt.Println("hello from goroutine")
}()
// 主goroutine继续执行
fmt.Println("Hello from main")
//多个Goroutine
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Printf("Goroutine %d\n", n)
}(i)
}
//闭包捕获变量
//传递参数
for i := 0; i < 5; i++ {
go func(i int) {
fmt.Printf("Goroutine %d\n", i)
}(i)
}
// 创建副本
for i := 0; i < 5; i++ {
i := i // 创建i的副本
go func() {
fmt.Printf("Goroutine %d\n", i)
}()
}
// 禁止main退出,goroutine没机会执行
done := make(chan bool)
go func() {
time.Sleep(time.Second)
fmt.Println("Goroutine done")
done <- true
}()
<-done //等待完成
//有始有终。
//goroutine泄漏
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println("val", val)
case <-time.After(time.Second):
fmt.Println("timeout")
}
}()
//等待Goroutine完成
//WaitGroup
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1) // 计数器+1
go func() {
defer wg.Done() // 计数器-1
fmt.Printf("Goroutine %d\n", i)
}()
}
wg.Wait() //等待计数器归零
//Channel
done = make(chan bool)
go func() {
fmt.Println("Goroutine done")
time.Sleep(time.Second)
done <- true
}()
<-done
// Context
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel() // 有始有终
go func() {
for {
select {
case <-ctx.Done():
return
default:
fmt.Println("Working...")
time.Sleep(200 * time.Millisecond)
}
}
}()
<-ctx.Done()
//errgroup.WithContext
// 创建一个带有上下文的 errgroup.Group 实例
g, ctx := errgroup.WithContext(context.Background())
// 使用 g.Go 来启动 goroutine
g.Go(func() error {
// 模拟耗时操作
time.Sleep(2 * time.Second)
fmt.Println("goroutine 1 finished")
return nil
})
g.Go(func() error {
// 模拟耗时操作
time.Sleep(1 * time.Second)
fmt.Println("goroutine 2 finished")
return nil
})
// 等待所有 goroutine 完成,并处理可能发生的错误
if err := g.Wait(); err != nil {
fmt.Println("An error occurred:", err)
} else {
fmt.Println("All goroutines completed successfully")
}
tasks := make([]int, 10)
//errgroup.SetLimit
g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(10)
for _, item := range tasks {
item := item
g.Go(func() error {
fmt.Printf("processing item %d\n", item)
// 模拟处理任务
return nil // 或者返回错误
})
}
if err := g.Wait(); err != nil {
fmt.Printf("batch failed %s %s", "err", err)
}
//x/sync/semaphore
sem := semaphore.NewWeighted(10)
for _, item := range tasks {
// Acquire 会阻塞直到拿到权重,或 ctx 取消
if err := sem.Acquire(ctx, 1); err != nil {
break // context 取消了,停止
}
go func(it int) {
defer sem.Release(1)
fmt.Printf("processing item %d\n", it)
// 模拟处理任务
// 处理完成后会自动释放权重
}(item)
}
// 等待所有 goroutine 完成
err := sem.Acquire(ctx, 10)
if err != nil {
return
}
// 等待goroutine执行完成
time.Sleep(time.Second)
}
Goroutine的核心要点:
| 特性 | 说明 |
|---|---|
| 轻量级 | 2KB栈空间 |
| 高并发 | 百万级goroutine |
| 简单 | go关键字启动 |
| 调度 | Go运行时自动调度 |
使用建议:
- 用WaitGroup等待完成
- 避免闭包陷阱
- 控制goroutine数量
- 使用context控制生命周期
- 避免goroutine泄漏
记住:
- 不要通过共享内存来通信,而应该通过通信来共享内存
- 主goroutine退出,所有goroutine都会终止
- goroutine很便宜,但不是免费的