在 Go 语言中,主协程(main goroutine)需要等待其他 goroutine 完成任务后再继续执行或退出程序,这是一个常见的并发同步需求。Go 提供了几种机制来实现这一点,具体取决于场景和需求。
方法 1:使用 sync.WaitGroup
sync.WaitGroup
是 Go 中最常用的同步工具,用于等待一组 goroutine 完成任务。它通过计数器机制工作,非常适合主协程等待多个子协程的情况。
示例代码
go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 启动 3 个 goroutine
for i := 1; i <= 3; i++ {
wg.Add(1) // 计数器加 1
go func(id int) {
defer wg.Done() // 任务完成后计数器减 1
fmt.Printf("Goroutine %d is running\n", id)
}(i)
}
wg.Wait() // 主协程等待所有 goroutine 完成
fmt.Println("All goroutines finished")
}
输出(顺序可能不同):
sql
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
工作原理:
wg.Add(n)
:增加计数器,表示需要等待的 goroutine 数量。
wg.Done()
:每个 goroutine 完成时调用,计数器减 1。
wg.Wait()
:主协程阻塞,直到计数器归零。
优点: 简单易用,适合固定数量的 goroutine。无需额外通道,性能开销低。
方法 2:使用通道(Channel)
通过通道传递信号,主协程可以等待所有 goroutine 发送完成信号。这种方法更灵活,但通常比 WaitGroup
稍复杂。
示例代码
go
package main
import "fmt"
func main() {
done := make(chan struct{}) // 用于通知完成的信号通道
numGoroutines := 3
for i := 1; i <= numGoroutines; i++ {
go func(id int) {
fmt.Printf("Goroutine %d is running\n", id)
done <- struct{}{} // 任务完成后发送信号
}(i)
}
// 等待所有 goroutine 完成
for i := 0; i < numGoroutines; i++ {
<-done // 接收信号
}
fmt.Println("All goroutines finished")
}
-
输出 (类似
WaitGroup
示例)。 -
工作原理:
- 每个 goroutine 在完成时向
done
通道发送一个信号。 - 主协程通过接收指定次数的信号来确认所有任务完成。
- 每个 goroutine 在完成时向
-
优点:
- 灵活性高,可以携带数据(例如任务结果)。
- 适合动态数量的 goroutine。
-
缺点:
- 需要手动管理接收次数,代码稍显繁琐。
方法 3:结合 context
控制退出
使用 context.Context
可以优雅地控制 goroutine 的退出,并让主协程等待所有任务完成。这种方法特别适合需要取消或超时的场景。
示例代码
go
package main
import (
"context"
"fmt"
"sync"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
select {
case <-ctx.Done():
fmt.Printf("Goroutine %d cancelled\n", id)
return
default:
fmt.Printf("Goroutine %d is running\n", id)
}
}(i)
}
// 模拟任务完成
cancel() // 发送取消信号
wg.Wait() // 等待所有 goroutine 退出
fmt.Println("All goroutines finished")
}
输出(可能因取消时机不同而变):
sql
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
-
工作原理:
context
用于通知 goroutine 退出。WaitGroup
确保主协程等待所有 goroutine 完成。
-
优点:
- 支持取消和超时,适合复杂并发场景。
-
缺点:
- 代码复杂度稍高。
方法 4:使用 errgroup
(推荐)
golang.org/x/sync/errgroup
是一个高级工具,结合了 WaitGroup
的等待功能和错误处理,特别适合需要等待一组任务并处理错误的情况。
示例代码
go
package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
var g errgroup.Group
for i := 1; i <= 3; i++ {
id := i
g.Go(func() error {
fmt.Printf("Goroutine %d is running\n", id)
return nil // 无错误
})
}
if err := g.Wait(); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("All goroutines finished")
}
}
输出:
sql
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
-
工作原理:
g.Go()
启动一个 goroutine,并将其加入等待组。g.Wait()
等待所有 goroutine 完成,并返回第一个非空错误(如果有)。
-
优点:
- 简单优雅,支持错误传播。
- 内置上下文支持(可通过
errgroup.WithContext
)。
-
安装:
- 需要
go get golang.org/x/sync/errgroup
。
- 需要
选择哪种方法?
方法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
sync.WaitGroup |
固定数量的简单任务 | 简单高效 | 不支持错误或取消 |
通道 | 动态任务或需要传递结果 | 灵活性高 | 手动管理复杂 |
context |
需要取消或超时的复杂场景 | 支持取消和超时 | 代码稍复杂 |
errgroup |
需要错误处理和等待的现代应用 | 优雅、功能强大 | 需要额外依赖 |
面试可能追问
- "为什么主协程不直接 sleep?"
time.Sleep
是固定延迟,无法准确等待任务完成,可能导致过早退出或不必要的等待。同步工具更可靠。 - "
WaitGroup
和通道有什么区别?"WaitGroup
是计数器机制,专注于等待;通道是通信机制,可以传递数据,但需要手动同步。 - "如何处理 goroutine 中的错误?" 可以用通道返回错误,或使用
errgroup
统一收集。
总结
主协程等待其他协程的最常用方法是 sync.WaitGroup
,简单高效。如果需要错误处理或取消功能,推荐 errgroup
或结合 context
。根据具体需求选择合适的工具,确保程序逻辑清晰且无泄露。