面试官:在go语言中,主协程如何等待其余协程完毕再操作?

451 阅读4分钟

在 Go 语言中,主协程(main goroutine)需要等待其他 goroutine 完成任务后再继续执行或退出程序,这是一个常见的并发同步需求。Go 提供了几种机制来实现这一点,具体取决于场景和需求。

方法 1:使用 sync.WaitGroup

sync.WaitGroup 是 Go 中最常用的同步工具,用于等待一组 goroutine 完成任务。它通过计数器机制工作,非常适合主协程等待多个子协程的情况。

示例代码

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")
}

输出(顺序可能不同):

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 稍复杂。

示例代码

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。
  • 缺点

    • 需要手动管理接收次数,代码稍显繁琐。

方法 3:结合 context 控制退出

使用 context.Context 可以优雅地控制 goroutine 的退出,并让主协程等待所有任务完成。这种方法特别适合需要取消或超时的场景。

示例代码

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")
}

输出(可能因取消时机不同而变):

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 的等待功能和错误处理,特别适合需要等待一组任务并处理错误的情况。

示例代码

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")
    }
}

输出

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需要错误处理和等待的现代应用优雅、功能强大需要额外依赖

面试可能追问

  1. “为什么主协程不直接 sleep?” time.Sleep 是固定延迟,无法准确等待任务完成,可能导致过早退出或不必要的等待。同步工具更可靠。
  2. WaitGroup 和通道有什么区别?” WaitGroup 是计数器机制,专注于等待;通道是通信机制,可以传递数据,但需要手动同步。
  3. “如何处理 goroutine 中的错误?” 可以用通道返回错误,或使用 errgroup 统一收集。

总结

主协程等待其他协程的最常用方法是 sync.WaitGroup,简单高效。如果需要错误处理或取消功能,推荐 errgroup 或结合 context。根据具体需求选择合适的工具,确保程序逻辑清晰且无泄露。