编程 深入理解 Go 并发操作:通过示例讲解 sync.WaitGroup 和 Context 的使用

2024-11-19 05:04:11 +0800 CST views 478

深入理解 Go 并发操作:通过示例讲解 sync.WaitGroup 和 Context 的使用

在 Go 语言中,并发编程是一个重要的特性。当一个任务可以被拆分为多个相互独立的子任务,并且这些子任务没有先后执行顺序的限制,需要等到所有子任务执行完毕后再进行下一步处理时,适合使用 sync.WaitGroup

然而,虽然 sync.WaitGroup 使用起来相对简单,但如果不小心,可能会踩到一些坑。本文将通过示例深入讲解如何正确使用 sync.WaitGroup,并介绍在更复杂的并发场景下如何结合 context 来处理任务取消。

正确使用 sync.WaitGroup

假设有一个任务需要执行 3 个子任务,可以使用 sync.WaitGroup 来等待所有子任务完成:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(3)

    go handleTask1(&wg)
    go handleTask2(&wg)
    go handleTask3(&wg)

    wg.Wait()

    fmt.Println("所有任务执行完毕.")
}

func handleTask1(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("执行任务 1")
}

func handleTask2(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("执行任务 2")
}

func handleTask3(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("执行任务 3")
}

执行输出:

执行任务 1
执行任务 3
执行任务 2
所有任务执行完毕.

注意,任务的执行顺序可能会有所不同,因为 Goroutine 是并发运行的。

sync.WaitGroup 使用注意事项

1. 传递指针

正确做法:

go handleTask1(&wg)

错误做法:

go handleTask1(wg)

在启动子任务时,必须传递 *sync.WaitGroup 的指针类型。如果传值,会导致每个子任务都有自己的副本,无法正确同步。

2. 确保 wg.Add() 在启动 Goroutine 之前执行

错误示例:

var wg sync.WaitGroup

go handleTask1(&wg)

wg.Wait()

// ...

func handleTask1(wg *sync.WaitGroup) {
    wg.Add(1)
    defer wg.Done()
    fmt.Println("执行任务 1")
}

在上述代码中,wg.Add(1) 在子 Goroutine 内部执行,但主 Goroutine 已经调用了 wg.Wait(),这可能导致主 Goroutine 提前结束。应当确保 wg.Add() 在启动 Goroutine 之前执行。

3. 保持计数器一致

注意 wg.Add()wg.Done() 的调用次数应当一致。wg.Done() 实际上等价于 wg.Add(-1)

更复杂的场景:任务失败时取消其他任务

sync.WaitGroup 适用于等待所有子任务完成的场景,但如果需要在某个子任务失败时,立即取消其他正在执行的子任务,就需要结合 context 来实现。

使用 sync.WaitGroup 和 Context

以下示例展示了如何在一个子任务失败时,取消其他所有子任务:

package main

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

func handleTask(ctx context.Context, taskID int, wg *sync.WaitGroup, cancel context.CancelFunc) {
    defer wg.Done()

    fmt.Printf("任务 %d 正在处理...\n", taskID)

    // 模拟任务处理,任务 1 将模拟失败
    if taskID == 1 {
        fmt.Printf("任务 %d 失败\n", taskID)
        cancel() // 取消上下文,通知其他任务停止
        return
    }

    // 检查上下文是否已取消
    select {
    case <-ctx.Done():
        fmt.Printf("任务 %d 已被取消\n", taskID)
        return
    default:
        // 模拟耗时操作
        time.Sleep(3 * time.Second)
        fmt.Printf("任务 %d 成功完成\n", taskID)
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup

    wg.Add(3)
    go handleTask(ctx, 1, &wg, cancel)
    go handleTask(ctx, 2, &wg, cancel)
    go handleTask(ctx, 3, &wg, cancel)

    wg.Wait()
    fmt.Println("所有任务已结束")
}

输出示例:

任务 1 正在处理...
任务 2 正在处理...
任务 3 正在处理...
任务 1 失败
任务 2 已被取消
任务 3 已被取消
所有任务已结束

解释

  1. 任务 1 开始处理:

    • 输出 任务 1 正在处理...
    • 由于 taskID == 1,模拟任务失败,输出 任务 1 失败
    • 调用 cancel(),取消上下文 ctx,通知其他任务停止。
  2. 任务 2 和任务 3 开始处理:

    • 输出 任务 2 正在处理...任务 3 正在处理...
    • 检查上下文 ctx,发现已被取消。
    • 输出 任务 2 已被取消任务 3 已被取消,然后退出。
  3. 等待所有任务结束:

    • 主 Goroutine 调用 wg.Wait(),等待所有子任务完成。
    • 所有子任务结束后,输出 所有任务已结束

通过这种方式,可以确保在任一子任务失败时,其他子任务能够及时检测到取消信号并停止执行,避免浪费资源。

小结

  • 使用 sync.WaitGroup 时,确保传递的是指针,且在启动 Goroutine 之前调用 wg.Add()
  • 在需要控制子任务的执行,如在某个任务失败时取消其他任务的场景下,结合 context 使用可以更好地管理并发任务。
  • sync.WaitGroup 适用于等待所有子任务完成的简单场景;对于更复杂的并发控制,context 提供了强大的功能。

通过对 sync.WaitGroupcontext 的灵活运用,可以更有效地管理 Go 程序中的并发操作,提高程序的健壮性和效率。

复制全文 生成海报 Go语言 并发编程 编程技巧

推荐文章

支付页面html收银台
2025-03-06 14:59:20 +0800 CST
实用MySQL函数
2024-11-19 03:00:12 +0800 CST
纯CSS绘制iPhoneX的外观
2024-11-19 06:39:43 +0800 CST
Gin 框架的中间件 代码压缩
2024-11-19 08:23:48 +0800 CST
Git 常用命令详解
2024-11-18 16:57:24 +0800 CST
一些好玩且实用的开源AI工具
2024-11-19 09:31:57 +0800 CST
OpenCV 检测与跟踪移动物体
2024-11-18 15:27:01 +0800 CST
Vue 中如何处理跨组件通信?
2024-11-17 15:59:54 +0800 CST
ElasticSearch简介与安装指南
2024-11-19 02:17:38 +0800 CST
thinkphp分页扩展
2024-11-18 10:18:09 +0800 CST
Vue3中如何处理组件的单元测试?
2024-11-18 15:00:45 +0800 CST
java MySQL如何获取唯一订单编号?
2024-11-18 18:51:44 +0800 CST
PHP解决XSS攻击
2024-11-19 02:17:37 +0800 CST
为什么大厂也无法避免写出Bug?
2024-11-19 10:03:23 +0800 CST
api接口怎么对接
2024-11-19 09:42:47 +0800 CST
程序员茄子在线接单