Go语言中WaitGroup并发同步利器

 更新时间:2026年05月10日 11:10:57   作者:码龙大大  
本文主要介绍了Go语言中WaitGroup并发同步利器,从其基本概念、用法、原理到高级用法和最佳实践,通过合理使用WaitGroup,能够提高并发程序的安全性和效率

1. WaitGroup的基本概念

WaitGroup是Go语言中用于实现并发同步的一种同步原语,它可以等待一组协程完成。WaitGroup是Go语言并发编程中最常用的同步机制之一,它可以帮助开发者协调多个协程的执行,确保所有协程都完成后再继续执行主协程。

Go语言的WaitGroup设计简洁而强大,它可以帮助开发者避免使用忙等或睡眠等不优雅的同步方式。本文将详细介绍Go语言中的WaitGroup,从原理到实践,帮助开发者更好地理解和使用WaitGroup。

2. WaitGroup的基本用法

2.1 创建和使用WaitGroup

package main

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

func main() {
	var wg sync.WaitGroup
	
	// 启动多个协程
	for i := 0; i < 5; i++ {
		wg.Add(1) // 增加计数器
		go func(id int) {
			defer wg.Done() // 减少计数器
			fmt.Printf("Goroutine %d started\n", id)
			time.Sleep(1 * time.Second)
			fmt.Printf("Goroutine %d finished\n", id)
		}(i)
	}
	
	fmt.Println("Waiting for all goroutines to finish...")
	wg.Wait() // 等待所有协程完成
	fmt.Println("All goroutines finished")
}

2.2 WaitGroup的方法

  • Add(delta int):增加计数器的值
  • Done():减少计数器的值,相当于Add(-1)
  • Wait():等待计数器的值变为0

2.3 WaitGroup的特性

  • 计数器:WaitGroup内部维护一个计数器,用于跟踪未完成的协程数量
  • 阻塞性:Wait()方法会阻塞,直到计数器的值变为0
  • 不可重用:WaitGroup在计数器变为0后,不能再次使用

3. WaitGroup的原理

3.1 WaitGroup的底层实现

WaitGroup在底层是通过互斥锁和条件变量实现的,它包含以下几个部分:

  • 计数器:用于跟踪未完成的协程数量
  • 互斥锁:用于保护计数器的并发访问
  • 条件变量:用于实现等待和唤醒机制

3.2 WaitGroup的工作原理

  1. Add操作

    • 加锁
    • 增加计数器的值
    • 解锁
  2. Done操作

    • 加锁
    • 减少计数器的值
    • 如果计数器变为0,唤醒所有等待的协程
    • 解锁
  3. Wait操作

    • 加锁
    • 如果计数器不为0,等待条件变量
    • 解锁

4. WaitGroup的高级用法

4.1 WaitGroup与错误处理

package main

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

func main() {
	var wg sync.WaitGroup
	errCh := make(chan error, 5)
	
	// 启动多个协程
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d started\n", id)
			time.Sleep(1 * time.Second)
			
			// 模拟错误
			if id == 2 {
				errCh <- fmt.Errorf("error in goroutine %d", id)
				return
			}
			
			fmt.Printf("Goroutine %d finished\n", id)
		}(i)
	}
	
	// 等待所有协程完成
	go func() {
		wg.Wait()
		close(errCh)
	}()
	
	// 收集错误
	for err := range errCh {
		fmt.Println("Error:", err)
	}
	
	fmt.Println("All goroutines finished")
}

4.2 WaitGroup与Context

package main

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

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()
	
	var wg sync.WaitGroup
	
	// 启动多个协程
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d started\n", id)
			
			// 检查上下文是否取消
			select {
			case <-ctx.Done():
				fmt.Printf("Goroutine %d cancelled\n", id)
				return
			case <-time.After(2 * time.Second):
				fmt.Printf("Goroutine %d finished\n", id)
			}
		}(i)
	}
	
	// 等待所有协程完成或上下文取消
	wg.Wait()
	fmt.Println("All goroutines finished")
}

4.3 WaitGroup与通道

package main

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

func main() {
	var wg sync.WaitGroup
	resultCh := make(chan int, 5)
	
	// 启动多个协程
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d started\n", id)
			time.Sleep(1 * time.Second)
			resultCh <- id * 2
			fmt.Printf("Goroutine %d finished\n", id)
		}(i)
	}
	
	// 等待所有协程完成
	go func() {
		wg.Wait()
		close(resultCh)
	}()
	
	// 收集结果
	for result := range resultCh {
		fmt.Println("Result:", result)
	}
	
	fmt.Println("All goroutines finished")
}

5. WaitGroup的最佳实践

5.1 正确使用Add方法

  • 在启动协程前调用Add:确保在启动协程前调用Add方法,避免计数器为0时Wait()方法立即返回
  • 使用正数增量:Add方法的参数应该是正数,表示要等待的协程数量
  • 避免负数增量:不要使用负数作为Add方法的参数,应该使用Done()方法来减少计数器

5.2 正确使用Done方法

  • 在协程结束时调用Done:确保每个协程在结束时都调用Done方法,减少计数器的值
  • 使用defer调用Done:使用defer语句确保即使协程发生错误,Done方法也会被调用

5.3 正确使用Wait方法

  • 在所有协程启动后调用Wait:确保在所有协程启动后再调用Wait方法
  • 只调用一次Wait:WaitGroup的Wait方法只能调用一次,在计数器变为0后,WaitGroup不能再次使用

5.4 错误处理

  • 使用通道收集错误:通过通道收集协程执行过程中产生的错误
  • 使用Context控制超时:使用Context控制协程的执行时间,避免协程无限阻塞

5.5 性能优化

  • 避免过多的协程:根据系统的CPU核心数和任务的性质,合理控制协程的数量
  • 避免长时间阻塞:避免协程长时间阻塞,影响其他协程的执行

6. WaitGroup的常见问题与解决方案

6.1 计数器不匹配

问题:Add方法的调用次数与Done方法的调用次数不匹配,导致计数器不为0,Wait()方法无限阻塞。

解决方案

  • 确保每个Add调用都有对应的Done调用
  • 使用defer语句确保Done方法一定会被调用

6.2 重复使用WaitGroup

问题:在Wait()方法返回后,再次使用同一个WaitGroup。

解决方案

  • 为每个需要同步的协程组创建一个新的WaitGroup
  • 不要在Wait()方法返回后再次使用同一个WaitGroup

6.3 协程泄露

问题:协程在执行过程中发生阻塞,导致Done方法没有被调用,计数器不为0,Wait()方法无限阻塞。

解决方案

  • 使用Context控制协程的生命周期
  • 设置合理的超时时间
  • 避免协程无限阻塞

6.4 性能问题

问题:启动过多的协程,导致系统资源耗尽,性能下降。

解决方案

  • 合理控制协程的数量
  • 使用协程池管理协程
  • 避免启动不必要的协程

7. WaitGroup的实战应用

7.1 并行处理任务

package main

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

func processTask(id int) {
	fmt.Printf("Processing task %d\n", id)
	time.Sleep(1 * time.Second)
	fmt.Printf("Task %d processed\n", id)
}

func main() {
	var wg sync.WaitGroup
	tasks := []int{1, 2, 3, 4, 5}
	
	// 并行处理任务
	for _, task := range tasks {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			processTask(id)
		}(task)
	}
	
	// 等待所有任务完成
	fmt.Println("Waiting for all tasks to complete...")
	wg.Wait()
	fmt.Println("All tasks completed")
}

7.2 并发下载文件

package main

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

func downloadFile(url string) {
	fmt.Printf("Downloading %s\n", url)
	time.Sleep(2 * time.Second) // 模拟下载时间
	fmt.Printf("Downloaded %s\n", url)
}

func main() {
	var wg sync.WaitGroup
	urls := []string{
		"https://example.com/file1.txt",
		"https://example.com/file2.txt",
		"https://example.com/file3.txt",
		"https://example.com/file4.txt",
		"https://example.com/file5.txt",
	}
	
	// 并发下载文件
	for _, url := range urls {
		wg.Add(1)
		go func(u string) {
			defer wg.Done()
			downloadFile(u)
		}(url)
	}
	
	// 等待所有下载完成
	fmt.Println("Waiting for all downloads to complete...")
	wg.Wait()
	fmt.Println("All downloads completed")
}

7.3 并发数据库操作

package main

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

func queryDatabase(id int) {
	fmt.Printf("Querying database for id %d\n", id)
	time.Sleep(500 * time.Millisecond) // 模拟数据库查询时间
	fmt.Printf("Query completed for id %d\n", id)
}

func main() {
	var wg sync.WaitGroup
	ids := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	
	// 并发查询数据库
	for _, id := range ids {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			queryDatabase(i)
		}(id)
	}
	
	// 等待所有查询完成
	fmt.Println("Waiting for all database queries to complete...")
	wg.Wait()
	fmt.Println("All database queries completed")
}

8. 总结

WaitGroup是Go语言中用于实现并发同步的一种同步原语,它可以等待一组协程完成。通过理解WaitGroup的原理和最佳实践,我们可以编写更加安全、高效的并发程序。

在使用WaitGroup时,应该注意以下几点:

  1. 正确使用Add、Done和Wait方法:确保Add方法的调用次数与Done方法的调用次数匹配
  2. 使用defer调用Done:确保即使协程发生错误,Done方法也会被调用
  3. 避免重复使用WaitGroup:在Wait()方法返回后,不要再次使用同一个WaitGroup
  4. 合理控制协程数量:根据系统的CPU核心数和任务的性质,合理控制协程的数量
  5. 正确处理错误:通过通道收集协程执行过程中产生的错误
  6. 使用Context控制超时:使用Context控制协程的执行时间,避免协程无限阻塞

通过合理使用WaitGroup,我们可以充分发挥Go语言的并发优势,构建高性能、可扩展的应用程序。WaitGroup是Go语言的一个重要特性,掌握它将有助于我们开发更加高效、可靠的Go应用。

到此这篇关于Go语言中WaitGroup并发同步利器的文章就介绍到这了,更多相关Go语言 WaitGroup并发同步内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:

相关文章

  • Go 语言入门学习之时间包

    Go 语言入门学习之时间包

    这篇文章主要介绍了Go 语言入门学习之时间包,GO 语言提供了 ​​time​​包来测量和显示时间,下文关于GO时间包的相关介绍需要的小伙伴可以参考一下
    2022-04-04
  • Go语言对JSON数据进行序列化和反序列化

    Go语言对JSON数据进行序列化和反序列化

    这篇文章介绍了Go语言对JSON数据进行序列化和反序列化的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • Golang之如何读取文件内容

    Golang之如何读取文件内容

    这篇文章主要介绍了Golang之如何读取文件内容问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • Golang使用sqlx操作sqlite3数据库的完整指南

    Golang使用sqlx操作sqlite3数据库的完整指南

    这篇文章主要为大家详细介绍了Golang使用sqlx操作sqlite3数据库的相关知识,本文会结合sqlx给出一些操作数据库的示例,并将常见功能封装了函数接口形式,希望对大家有所帮助
    2025-12-12
  • Golan中 new() 、 make() 和简短声明符的区别和使用

    Golan中 new() 、 make() 和简短声明符的区别和使用

    Go语言中的new()、make()和简短声明符的区别和使用,new()用于分配内存并返回指针,make()用于初始化切片、映射和通道,并返回初始化后的对象,简短声明符:=可以简化变量声明和初始化过程,感兴趣的朋友一起看看吧
    2025-01-01
  • 自动生成代码controller tool的简单使用

    自动生成代码controller tool的简单使用

    这篇文章主要为大家介绍了自动生成代码controller tool的简单使用示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-05-05
  • Golang初始化MySQL数据库方法浅析

    Golang初始化MySQL数据库方法浅析

    这篇文章主要介绍了Golang初始化MySQL数据库的方法,数据库的建立第一步即要初始化,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-05-05
  • golang gopm get -g -v 无法获取第三方库的解决方案

    golang gopm get -g -v 无法获取第三方库的解决方案

    这篇文章主要介绍了golang gopm get -g -v 无法获取第三方库的解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • 在ubuntu下安装go开发环境的全过程

    在ubuntu下安装go开发环境的全过程

    Go语言是谷歌公司开发的编程语言,虽然安装和配置go很简单,但是很多初学者在第一次安装go环境时会遇到各种坑,下面这篇文章主要给大家介绍了关于在ubuntu下安装go开发环境的相关资料,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2022-08-08
  • 浅谈Golang的方法传递值应该注意的地方

    浅谈Golang的方法传递值应该注意的地方

    这篇文章主要介绍了浅谈Golang的方法传递值应该注意的地方,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12

最新评论