Golang sync.WaitGroup 简介与用法

Posted 恋喵大鲤鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang sync.WaitGroup 简介与用法相关的知识,希望对你有一定的参考价值。

文章目录

1.简介

sync.WaitGroup 用于阻塞等待一组 Go 程的结束。主 Go 程调用 Add() 来设置等待的 Go 程数,然后该组中的每个 Go 程都需要在运行结束时调用 Done(), 递减 WaitGroup 的 Go 程计数器 counter。当 counter 变为 0 时,主 Go 程被唤醒继续执行。

type WaitGroup struct 
    // contains filtered or unexported fields


// 设置需要等待的 Go 程数量。
func (wg *WaitGroup) Add(delta int)

// Go 程计数器减 1。
func (wg *WaitGroup) Done()

// 阻塞等待所有 Go 程结束(等待 Go 程计数器变为 0)。
func (wg *WaitGroup) Wait()

WaitGroup 有三个方法,其中 Done() 调用了 Add(-1)。

标准用法:
(1)启动 Go 程时调用 Add();
(2)在 Go 程结束时调用 Done();
(3)最后调用 Wait()。

2.使用示例

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func main() 
	fmt.Println("entry main")

	wg.Add(2)
	go foo1()
	go foo2()

	fmt.Println("wg.Wait()")
	wg.Wait()

	fmt.Println("exit main")


func foo1() 
	defer wg.Done()
	fmt.Println("exit foo1")


func foo2() 
	defer wg.Done()
	fmt.Println("exit foo2")

编译运行输出:

entry main
wg.Wait()
exit foo2
exit foo1
exit main

注意: 多次执行输出结果中 “exit foo2” 和 “exit foo1” 的先后顺序是不定的,因为多协程并发执行的顺序是随机的。

3.错误示例

如果使用过程中通过 Add() 添加的 Go 程数与调用 Done() 的次数不符,即 sync.WaitGroup 的 Go 程计数器等所有子 Go 程结束后不为 0,则会引发 panic。

3.1 Done() 过多

func main() 
    fmt.Println("entry main")

    var wg sync.WaitGroup
    wg.Done()

    fmt.Println("wg.Wait()")
    wg.Wait()

    fmt.Println("exit main")

编译运行输出:

entry main
panic: sync: negative WaitGroup counter

goroutine 1 [running]:
sync.(*WaitGroup).Add(0xc4200140d0, 0xffffffffffffffff)
	/usr/lib/golang/src/sync/waitgroup.go:75 +0x134
sync.(*WaitGroup).Done(0xc4200140d0)
	/usr/lib/golang/src/sync/waitgroup.go:100 +0x34
main.main()
	/data/goTest/src/waitgroup/main.go:34 +0x8e

可见,当 Go 程计数器变为负数时,将引发 panic。

3.2 Done() 过少

注释掉 foo2() 中的 defer wg.Done(),使得 Go 程结束时不减少 sync.WaitGroup 的 Go 程计数器。

func foo2() 
    //defer wg.Done()
    fmt.Println("exit foo2")

编译运行输出:

entry main
wg.Wait()
exit foo1
exit foo2
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x54aa7c)
	/usr/lib/golang/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0x54aa70)
	/usr/lib/golang/src/sync/waitgroup.go:131 +0x72
main.main()
	/data/goTest/src/waitgroup/main.go:33 +0x10e

这个错误表明,在最后一个活动线程 foo2 退出的时候,Go 检测到当前没有还在运行的 Go 程,但主 Go 程仍在等待,发生了死锁现象,于是引发 panic,这是 Go 的一种自我保护机制。

4.errgroup

4.1 用法

鉴于使用 sync.WaitGroup 容易出错,Go 官方包 errgroup 对 sync.WaitGroup 做了进一步封装,不再需要 Add 和 Done,用起来更加方便。详见 golang.org/x/sync/errgroup

// A Group is a collection of goroutines working on subtasks that are part of
// the same overall task.
//
// A zero Group is valid and does not cancel on error.
type Group struct 
	cancel func()

	wg sync.WaitGroup

	errOnce sync.Once
	err     error

使用示例:

package main

import (
	"fmt"

	"golang.org/x/sync/errgroup"
)

func foo1() error 
	fmt.Println("exit foo1")
	return nil


func foo2() error 
	fmt.Println("exit foo2")
	return nil


func main() 
	fmt.Println("enter main")
	var g errgroup.Group
	g.Go(foo1)
	g.Go(foo2)

	fmt.Println("g.Wait()")
	g.Wait()

	fmt.Println("exit main")

运行输出:

enter main
wg.Wait()
exit foo2
exit foo1
exit main

4.2 问题

在 Golang 中,因为子协程内部发生的 panic 必须由子协程自己捕获,主协程无法捕获子协程的 panic,这将导致整个进程异常结束。

package main

import (
	"fmt"
	"time"
)

func main() 
	defer func() 
		if e := recover(); e != nil 
			fmt.Println("recover_panic")
		
	()

	go func() 
		panic("goroutine2_panic")
	()

	time.Sleep(2 * time.Second)

运行输出:

go run main.go
panic: goroutine2_panic

goroutine 6 [running]:
main.main.func2()
        D:/code/gotest/main.go:16 +0x27
created by main.main
        D:/code/gotest/main.go:15 +0x45
exit status 2

可见,主协程是无法捕获子协程的 panic。

回到 errgroup,虽然其使用起来非常方便,但有一个问题:当待执行的函数在子协程中发生 panic,errgroup 没有提供一个统一的捕获方式,如果待执行的函数自己没有捕获 panic 将会发生进程异常结束。

5.safegroup

为了解决 errgroup 没有捕获 panic 的缺陷,我们可以在 errgroup 基础之上再做一次封装,对待执行的函数进行统一的异常捕获。

package safegroup

import (
	"context"
	"errors"
	"fmt"
	"runtime"

	"golang.org/x/sync/errgroup"
)

// PanicBufLen panic 调用栈日志 buffer 大小。
var PanicBufLen = 2048

// Group panic 安全的 group 结构。
type Group struct 
	g errgroup.Group


// WithContext 生成带 context 的 group。
func WithContext(ctx context.Context) (*Group, context.Context) 
	g, ctx := errgroup.WithContext(ctx)
	return &Groupg: *g, ctx


// Go 启动一个协程,会接住 panic 并返回 err。
func (s *Group) Go(f func() error) 
	s.g.Go(func() (err error) 
		defer func() 
			if r := recover(); r != nil 
				buf := make([]byte, PanicBufLen)
				buf = buf[:runtime.Stack(buf, false)]
				fmt.Printf("[PANIC]:\\n%+v\\n[STACK]:\\n%s\\n", r, buf)
				err = errors.New(fmt.Sprint(r))
			
		()
		return f()
	)


// Wait 等待协程结束。
func (s *Group) Wait() error 
	return s.g.Wait()

下面我们利用 safegroup 安全并发地执行函数。

package main

import (
	"fmt"
	"main/safegroup"
)

func foo1() error 
	fmt.Println("exit foo1")
	panic("foo1 panic")
	return nil


func foo2() error 
	fmt.Println("exit foo2")
	return nil


func main() 
	var sg safegroup.Group
	sg.Go(foo1)
	sg.Go(foo2)
	err := sg.Wait()
	fmt.Println(err)
	fmt.Println("exit main")

运行输出:

exit foo2
exit foo1
[PANIC]:
foo1 panic
[STACK]:
goroutine 6 [running]:
main/safegroup.(*Group).Go.func1.1()
        D:/code/gotest/safegroup/safegroup.go:32 +0x85
panic(0xb96d80, 0xbc8ff0)
        C:/Program Files/Go/src/runtime/panic.go:1038 +0x215
main.foo1()
        D:/code/gotest/main.go:10 +0x65
main/safegroup.(*Group).Go.func1()
        D:/code/gotest/safegroup/safegroup.go:37 +0x63
golang.org/x/sync/errgroup.(*Group).Go.func1()
        D:/go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/errgroup/errgroup.go:57 +0x67
created by golang.org/x/sync/errgroup.(*Group).Go
        D:/go/pkg/mod/golang.org/x/sync@v0.0.0-20210220032951-036812b2e83c/errgroup/errgroup.go:54 +0x92

foo1 panic
exit main

利用 safegroup 可以捕获 panic,并打印协程栈信息,不会因为待执行的函数在子协程中发生 panic 而影响主协程。

6.go-huge-util

关于 safegroup 的实现,已放到开源库 go-huge-util,可 import 直接使用。

package main

import (
	"fmt"
	"github.com/dablelv/go-huge-util/safegroup"
)

func foo1() error 
	fmt.Println("exit foo1")
	panic("foo1 panic")
	return nil


func foo2() error 
	fmt.Println("exit foo2")
	return nil


func main() 
	var sg safegroup.Group
	sg.Go(foo1)
	sg.Go(foo2)
	err := sg.Wait()
	fmt.Println(err)
	fmt.Println("exit main")

7.小结

标准库 sync.WaitGroup 可阻塞等待一组 Go 程的结束,可用于一组协程的并发同步控制。使用上不是很方便,于是 Go 官方包 errgroup 对 sync.WaitGroup 做了进一步封装,不再需要 Add 和 Done,用起来更加方便。

errgroup 虽然用起来方便,但是其没有对子协程进行统一的 panic 捕获,很容易发生子协程 panic 导致主协程退出。

为了弥补 errgroup 的这点不足,我对 errgroup 做进一步的封装,推出 safegroup,已开源至 go-huge-util 供大家使用。


参考文献

Golang pkg/sync
ErrGroup - Go Packages
golang语言异步通信之WaitGroup

以上是关于Golang sync.WaitGroup 简介与用法的主要内容,如果未能解决你的问题,请参考以下文章

Golang WaitGroup源码分析

golang中sync.WaitGroup的使用

Golang的sync.WaitGroup 实现逻辑和源码解析

golang-----golang sync.WaitGroup解决goroutine同步

sync.WaitGroup的使用以及坑

Go并发控制之sync.WaitGroup