动态并发控制:sync.WaitGroup的灵活运用

简介: 动态并发控制:sync.WaitGroup的灵活运用

概述

在并发编程中,控制主程序等待所有 Goroutine 完成任务是一项关键任务。Go 语言提供了 sync.WaitGroup 来解决这一问题。

本文将讲解 sync.WaitGroup 的使用方法、原理以及在实际项目中的应用场景,用清晰的代码示例和详细的注释,助力读者掌握并发编程中等待组的使用技巧。


 

1. 基本使用

1

package main
import (  "fmt"  "sync"  "time")
func main() {  var wg sync.WaitGroup
  for i := 1; i <= 3; i++ {    wg.Add(1)    go worker(i, &wg)  }
  wg.Wait()  fmt.Println("All workers have completed.")}
func worker(id int, wg *sync.WaitGroup) {  defer wg.Done()
  fmt.Printf("Worker %d started\n", id)  time.Sleep(2 * time.Second)  fmt.Printf("Worker %d completed\n", id)}

在上面示例中,用一个 sync.WaitGroup 实例 wg,然后使用 wg.Add(1) 来增加计数,表示有一个 Goroutine 需要等待。

在每个 Goroutine 的结束处,使用 defer wg.Done() 来减少计数,表示一个 Goroutine 已完成。

最后,用 wg.Wait() 来等待所有 Goroutine 完成。

1.

package main
import (  "fmt"  "sync"  "time")
func main() {  var wg sync.WaitGroup
  for i := 1; i <= 3; i++ {    wg.Add(1)    go workerWithError(i, &wg)  }
  wg.Wait()  fmt.Println("All workers have completed.")}
func workerWithError(id int, wg *sync.WaitGroup) {  defer wg.Done()
  fmt.Printf("Worker %d started\n", id)  time.Sleep(2 * time.Second)
  // 模拟错误发生  if id == 2 {    fmt.Printf("Worker %d encountered an error\n", id)    return  }
  fmt.Printf("Worker %d completed\n", id)}

有时候,需要在 Goroutine 中处理错误。在这个示例中,当 id 为 2 时,模拟了一个错误的情况。

通过在错误发生时提前返回,可以确保计数正确减少,避免等待组出现死锁。


 

2. 多级等待组

2.1

package main
import (  "fmt"  "sync"  "time")
func main() {  var outerWG sync.WaitGroup  var innerWG sync.WaitGroup
  for i := 1; i <= 2; i++ {    outerWG.Add(1)    go outerWorker(i, &outerWG, &innerWG)  }
  outerWG.Wait()  fmt.Println("All outer workers have completed.")}
func outerWorker(id int, outerWG, innerWG *sync.WaitGroup) {  defer outerWG.Done()
  fmt.Printf("Outer Worker %d started\n", id)
  for j := 1; j <= 3; j++ {    innerWG.Add(1)    go innerWorker(id, j, innerWG)  }
  innerWG.Wait()  fmt.Printf("Outer Worker %d completed\n", id)}
func innerWorker(outerID, innerID int, wg *sync.WaitGroup) {  defer wg.Done()
  fmt.Printf("Inner Worker %d of Outer Worker %d started\n", innerID, outerID)  time.Sleep(2 * time.Second)  fmt.Printf("Inner Worker %d of Outer Worker %d completed\n", innerID, outerID)}

在示例中,使用了嵌套的 sync.WaitGroup

外部的等待组 outerWG 等待所有外部 Goroutine 完成,而每个外部 Goroutine 内部的 innerWG 则等待其内部的所有 Goroutine 完成。

2.

package main
import (  "fmt"  "sync"  "time")
func main() {  var dynamicWG sync.WaitGroup
  for i := 1; i <= 3; i++ {    dynamicWG.Add(1)    go dynamicWorker(i, &dynamicWG)  }
  // 模拟动态添加更多任务  time.Sleep(1 * time.Second)    for i := 4; i <= 6; i++ {    dynamicWG.Add(1)    go dynamicWorker(i, &dynamicWG)  }
  dynamicWG.Wait()    fmt.Println("All dynamic workers have completed.")}
func dynamicWorker(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Printf("Dynamic Worker %d started\n", id)  time.Sleep(2 * time.Second)  fmt.Printf("Dynamic Worker %d completed\n", id)}

在上述示例中,创建了一个等待组 dynamicWG,然后在运行时动态添加了更多的任务。

用这种方式,可以动态地管理需要等待的 Goroutine 数量。


 

3. 超时处理

3.1


package main
import (  "fmt"  "sync"  "time")
func main() {
  var timeoutWG sync.WaitGroup
  for i := 1; i <= 3; i++ {    timeoutWG.Add(1)    go timeoutWorker(i, &timeoutWG)  }
  // 等待最多5秒,超时则不再等待  timeout := time.After(5 * time.Second)    done := make(chan struct{})
  go func() {    timeoutWG.Wait()    close(done)  }()
  select {  case <-done:    fmt.Println("All timeout workers have completed.")    case <-timeout:    fmt.Println("Timeout reached. Not all workers have completed.")  }}
func timeoutWorker(id int, wg *sync.WaitGroup) {  defer wg.Done()
  fmt.Printf("Timeout Worker %d started\n", id)    time.Sleep(time.Duration(id) * time.Second)    fmt.Printf("Timeout Worker %d completed\n", id)  }

在上面示例中,用 time.After 创建了一个 5 秒的超时通道。

在另一个 Goroutine 中监听等待组的完成情况,可以在超时或任务完成时得知等待的最终结果。

3.2

package main
import (  "errors"  "fmt"  "sync"  "time")
func main() {  var timeoutWG sync.WaitGroup
  for i := 1; i <= 3; i++ {    timeoutWG.Add(1)    go timeoutWorkerWithError(i, &timeoutWG)  }
  // 等待最多5秒,超时则返回错误  err := waitWithTimeout(&timeoutWG, 5*time.Second)
  if err != nil {    fmt.Printf("Timeout reached. Not all workers have completed. Error: %v\n", err)  } else {    fmt.Println("All timeout workers have completed.")  }}
func timeoutWorkerWithError(id int, wg *sync.WaitGroup) {  defer wg.Done()
  fmt.Printf("Timeout Worker %d started\n", id)  time.Sleep(time.Duration(id) * time.Second)
  // 模拟错误发生  if id == 2 {    fmt.Printf("Timeout Worker %d encountered an error\n", id)    return  }
  fmt.Printf("Timeout Worker %d completed\n", id)}
func waitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) error {  done := make(chan struct{})
  go func() {    defer close(done)    wg.Wait()  }()
  select {  case <-done:    return nil      case <-time.After(timeout):    return errors.New("timeout reached")  }}

有时候,希望在程序超时的时候返回一个错误。

在这个示例中,用封装等待组的超时检查,可以在主程序中获得一个清晰的错误提示。


 

总结

通过讨论 sync.WaitGroup 的基本用法、避免常见错误以及实际应用,深入了解了这个强大的同步工具。

在 Go 语言并发编程中,合理使用 sync.WaitGroup 能够优雅地处理并发等待,确保主程序在所有任务完成后再继续执行。

目录
相关文章
|
3月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
69 1
|
3月前
|
设计模式 Java Go
Go - 使用 sync.WaitGroup 来实现并发操作
Go - 使用 sync.WaitGroup 来实现并发操作
44 2
|
6月前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
62 0
|
6月前
|
消息中间件 Java 关系型数据库
面试官:说说MVCC的执行原理?
面试官:说说MVCC的执行原理?
124 1
面试官:说说MVCC的执行原理?
|
Go
Go并发控制sync.WaitGroup
Go语言使用sync.WaitGroup控制并发的顺序,用于任务的同步,等待所有并发任务完成,再持续执行。
69 0
|
6月前
|
安全 Java
Java并发编程—并发流程控制与AQS原理及相关源码解析
Java并发编程—并发流程控制与AQS原理及相关源码解析
76 0
|
6月前
|
Java
多线程并发之显示锁Lock与其通信方式Condition源码解读
多线程并发之显示锁Lock与其通信方式Condition源码解读
47 0
|
消息中间件 NoSQL Redis
深入探究Redis事务和Lua脚本:实现原子操作与复杂业务逻辑
本篇深入剖析了Redis的事务处理和Lua脚本特性,为读者呈现了如何利用这两个功能来实现数据的原子操作和执行复杂的业务逻辑。我们首先介绍了Redis事务的概念和基本操作,通过MULTI、EXEC、DISCARD和WATCH等命令,展示了如何在一组命令中保持原子性。进一步,我们探讨了事务命令的使用方法,演示了如何在事务中监视键变化以及提交事务。
789 0
|
存储 缓存 Go
sync.singleflight 到底怎么用才对?
sync.singleflight 到底怎么用才对?
117 0