如何使用 WaitGroup 进行 goroutine 同步

GolangBeginner
立即练习

简介

Go 语言的 sync.WaitGroup 是一个强大的并发控制原语,它允许你同步多个 goroutine 的执行。在本教程中,我们将探讨 WaitGroup 的基本原理,以及如何利用它来构建健壮且高效的并发应用程序。

精通 Go 语言的 WaitGroup

Go 语言的 sync.WaitGroup 是一个强大的并发控制原语,它允许你同步多个 goroutine 的执行。在本节中,我们将探讨 WaitGroup 的基本原理,以及如何利用它来构建健壮且高效的并发应用程序。

理解 WaitGroup 的基础

Go 语言中的 sync.WaitGroup 类型提供了一种方法来等待一组 goroutine 完成执行。它维护一个计数器,该计数器表示当前正在运行的 goroutine 的数量。当一个新的 goroutine 添加到 WaitGroup 时,计数器会增加,当一个 goroutine 完成时,计数器会减少。

以下是一个简单的示例,展示了 WaitGroup 的基本用法:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // 向 WaitGroup 添加 3 个 goroutine
    wg.Add(3)

    // 启动 goroutine
    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 1 正在工作...")
    }()

    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 2 正在工作...")
    }()

    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 3 正在工作...")
    }()

    // 等待所有 goroutine 完成
    wg.Wait()

    fmt.Println("所有 goroutine 都已完成。")
}

在这个示例中,我们创建了一个 WaitGroup 并向其中添加了 3 个 goroutine。每个 goroutine 在完成时调用 wg.Done() 来减少计数器。wg.Wait() 函数会阻塞主 goroutine,直到计数器达到 0,确保所有添加的 goroutine 都已完成。

在实际场景中应用 WaitGroup

WaitGroup 在需要协调多个异步任务执行的场景中特别有用。例如,你可能会使用 WaitGroup 在处理结果之前等待一组网络请求完成。

以下是一个使用 WaitGroup 并发从多个 URL 获取数据的示例:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
)

func fetchURL(url string, wg *sync.WaitGroup) {
    defer wg.Done()
    resp, err := http.Get(url)
    if err!= nil {
        fmt.Printf("获取 %s 时出错: %v\n", url, err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err!= nil {
        fmt.Printf("读取来自 %s 的响应时出错: %v\n", url, err)
        return
    }
    fmt.Printf("从 %s 获取了 %d 字节\n", url, len(body))
}

func main() {
    var wg sync.WaitGroup
    urls := []string{
        "
        "
        "
    }

    for _, url := range urls {
        wg.Add(1)
        go fetchURL(url, &wg)
    }

    wg.Wait()
    fmt.Println("所有 URL 都已获取。")
}

在这个示例中,我们使用 WaitGroup 来协调对不同 URL 的多个 HTTP 请求的执行。fetchURL 函数负责从单个 URL 获取内容,并在完成时调用 wg.Done()。主 goroutine 使用 wg.Wait() 等待所有 goroutine 完成,然后打印最终消息。

通过使用 WaitGroup,我们可以确保主 goroutine 在所有网络请求都处理完毕之前不会退出,并且我们可以通过调整 urls 切片中的 URL 数量轻松扩展并发请求的数量。

理解 WaitGroup 的基本原理

Go 语言中的 sync.WaitGroup 是一个同步原语,它允许你等待一组 goroutine 完成执行。它维护一个内部计数器,该计数器表示活跃 goroutine 的数量,并提供三个主要方法:

  1. wg.Add(n int):向 WaitGroup 添加 n 个 goroutine。
  2. wg.Done():将 WaitGroup 计数器减 1,表示一个 goroutine 已完成。
  3. wg.Wait():阻塞调用的 goroutine,直到 WaitGroup 计数器达到 0,这意味着所有添加的 goroutine 都已完成。

以下是一个演示 WaitGroup 基本用法的示例:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // 向 WaitGroup 添加 3 个 goroutine
    wg.Add(3)

    // 启动 goroutine
    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 1 正在工作...")
    }()

    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 2 正在工作...")
    }()

    go func() {
        defer wg.Done()
        fmt.Println("Goroutine 3 正在工作...")
    }()

    // 等待所有 goroutine 完成
    wg.Wait()

    fmt.Println("所有 goroutine 都已完成。")
}

在这个示例中,我们创建了一个 WaitGroup 并向其中添加了 3 个 goroutine。每个 goroutine 在完成时调用 wg.Done() 来减少计数器。wg.Wait() 函数会阻塞主 goroutine,直到计数器达到 0,确保所有添加的 goroutine 都已完成。

WaitGroup 在需要协调多个异步任务执行的场景中特别有用,例如从多个源获取数据或处理一批作业。

使用 WaitGroup 处理恐慌和错误

在并发代码中使用 WaitGroup 时,考虑如何处理 goroutine 中可能发生的恐慌和错误非常重要。一种常见的方法是在 defer 语句中使用 defer wg.Done() 调用,以确保即使发生恐慌,WaitGroup 计数器也会减少。

以下是一个演示如何使用 WaitGroup 处理恐慌和错误的示例:

package main

import (
    "fmt"
    "sync"
)

func processItem(item int, wg *sync.WaitGroup) {
    defer wg.Done()

    // 模拟错误或恐慌
    if item%2 == 0 {
        panic(fmt.Sprintf("处理项目 %d 时出错", item))
    }

    fmt.Printf("已处理项目 %d\n", item)
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go processItem(i, &wg)
    }

    // 等待所有 goroutine 完成
    wg.Wait()

    fmt.Println("所有项目都已处理。")
}

在这个示例中,processItem 函数为偶数编号的项目模拟错误或恐慌。通过使用 defer wg.Done() 调用,我们确保即使发生恐慌,WaitGroup 计数器也会减少。主 goroutine 中的 wg.Wait() 调用将阻塞,直到所有 goroutine 完成,无论是否发生任何恐慌或错误。

通过正确处理恐慌和错误,你可以确保并发代码更加健壮,并能优雅地处理意外情况。

高级 WaitGroup 模式与技巧

虽然 sync.WaitGroup 的基本用法很简单,但有一些更高级的模式和技巧可以帮助你构建更健壮、更灵活的并发应用程序。在本节中,我们将探讨其中的一些模式和技巧。

使用 WaitGroup 限制并发

WaitGroup 的一个常见用例是限制并发操作的数量。当你有一组有限的资源(例如数据库连接、API 速率限制),并且不想使其不堪重负时,这会很有用。

以下是一个使用 WaitGroup 限制并发 HTTP 请求数量的示例:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func fetchURL(url string, wg *sync.WaitGroup, sem chan struct{}) {
    defer wg.Done()

    // 获取信号量中的一个槽位
    sem <- struct{}{}
    defer func() { <-sem }()

    resp, err := http.Get(url)
    if err!= nil {
        fmt.Printf("获取 %s 时出错: %v\n", url, err)
        return
    }
    defer resp.Body.Close()
    fmt.Printf("已获取 %s\n", url)
}

func main() {
    var wg sync.WaitGroup
    const maxConcurrency = 5

    // 创建一个信号量通道以限制并发
    sem := make(chan struct{}, maxConcurrency)

    urls := []string{
        "
        "
        "
        "
        "
        "
        "
    }

    for _, url := range urls {
        wg.Add(1)
        go fetchURL(url, &wg, sem)
    }

    wg.Wait()
    fmt.Println("所有 URL 都已获取。")
}

在这个示例中,我们使用一个带缓冲的通道 sem 作为信号量,将并发 HTTP 请求的数量限制为 maxConcurrency(在这种情况下为 5)。每个调用 fetchURL 的 goroutine 在发出请求之前必须获取信号量中的一个槽位,并在请求完成时释放该槽位。这确保我们不会因过多的并发请求而使系统不堪重负。

使用 WaitGroup 处理错误和取消

在更复杂的场景中使用 WaitGroup 时,考虑如何处理错误和取消很重要。一种方法是使用 context.Context 将取消信号传播到 goroutine 中。

以下是一个演示如何将 context.ContextWaitGroup 一起使用来处理错误和取消的示例:

package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

func processItem(ctx context.Context, item int, wg *sync.WaitGroup) {
    defer wg.Done()

    select {
    case <-ctx.Done():
        fmt.Printf("已取消处理项目 %d\n", item)
        return
    default:
        // 模拟处理项目
        fmt.Printf("正在处理项目 %d\n", item)
        time.Sleep(time.Second)
    }
}

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go processItem(ctx, i, &wg)
    }

    wg.Wait()
    fmt.Println("所有项目都已处理。")
}

在这个示例中,我们创建了一个带有 5 秒超时的 context.Context。然后,我们将这个上下文传递给每个调用 processItem 的 goroutine。如果上下文被取消(无论是由于超时而取消还是通过调用 cancel()),goroutine 将收到取消信号并优雅地退出。

通过将 context.ContextWaitGroup 一起使用,你可以构建更健壮的并发应用程序,能够更有效地处理错误和取消场景。

总结

在本教程中,你已经学习了 Go 语言的 sync.WaitGroup 的基础知识,以及如何使用它来协调多个异步任务的执行。你已经探讨了可以应用 WaitGroup 的实际场景,例如在处理结果之前等待一组网络请求完成。此外,你还发现了一些高级的 WaitGroup 模式和技巧,这些可以帮助你编写更高效、更可靠的并发代码。通过掌握 WaitGroup 的使用,你可以充分发挥 Go 语言并发特性的潜力,并构建高性能、可扩展的应用程序。