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的sync.WaitGroup 实现逻辑和源码解析