前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#71 Misusing sync.WaitGroup

Go语言中常见100问题-#71 Misusing sync.WaitGroup

作者头像
数据小冰
发布2022-08-15 15:26:56
2880
发布2022-08-15 15:26:56
举报
文章被收录于专栏:数据小冰
误用sync.WaitGroup

sync.WaitGroup是一种等待n个操作完成的机制,通常,我们使用它来等待n个goroutine完成。下面将学习它的使用方法,然后将看到一个高频错误使用问题,以及这个问题导致的不确定性行为。

下面的代码创建了一个sync.WaitGroup对象,并且为默认的零值。

代码语言:javascript
复制
wg := sync.WaitGroup{}

在内部实现上,sync.WaitGroup拥有一个默认初始化零的内部计数器。我们可以使用Add(int)方法增加这个计数器,使用Done()或者Add一个负数来减小计数器。最后需要知道的一点是,如果想等待计数器为零,必须使用Wait()方法,该方法在计数器不为零时会阻塞。

「NOTE:sync.WaitGroup计数器的值不能负数,否则会产生panic.」

下面的示例程序中,初始化了一个WaitGroup对象,启动3个goroutine并发的将v的值增加1,通过WaitGroup等待这3个goroutine完成。最后,当3个goroutine都执行完成后,打印计数器v的值(本应该打印3)。你能猜测这段代码是否存在问题?

代码语言:javascript
复制
wg := sync.WaitGroup{}
var v uint64

for i := 0; i < 3; i++ {
        go func() {
                wg.Add(1)
                atomic.AddUint64(&v, 1)
                wg.Done()
        }()
}

wg.Wait()
fmt.Println(v)

如果我们运行上面的代码,得到的是一个不确定的值,它可能打印0到3中的任何值。此外,如果加入-race启用数据竞争检查,在运行时甚至会捕获到存在数据竞争。我们使用的是sync/atomic原子包操作v自增,怎么可能存在数据竞争问题呢?到底是哪里有问题呢?

下面是上面的程序加入-race执行的结果:

代码语言:javascript
复制
─ go run -race example1.go                                                                                         
2
==================
WARNING: DATA RACE
Write at 0x00c00001c108 by goroutine 9:
  sync/atomic.AddInt64()
...
Previous read at 0x00c00001c108 by main goroutine:
  main.main()
...

上面代码存在的问题是wg.Add(1)操作在新的goroutine内部执行,而不是在父goroutine执行的。因此,这不能保证我们希望在调用wg.Wait()之前等待三个goroutine的本意。

下面是程序打印输出2的一个可能的执行流程。主goroutine启动了3个子goroutine。然而最后一个子goroutine是在前两个子goroutine已经调用wg.Done()之后执行的。此时,主goroutine调用wg.Done()不会被阻塞,当它读取v时,此时v的值为2.竞争检测器会检查到存在竞争问题,因为此时主goroutine对v有访问操作,而第三个子goroutine对v有修改操作。

在处理goroutine时,需要记住的一点是如果没有同步机制,goroutine之间的执行顺序是不确定的,像下面的程序可能打印ab也可能打印ba.

代码语言:javascript
复制
go func() {
        fmt.Print("a")
}()
go func() {
        fmt.Print("b")
}()

事实上,上面的两个goroutine可以分配给不同的线程,并且不能保证哪个线程会先执行。CPU必须使用所谓的内存栅栏(也称为内存屏障)来保证顺序。Go语言提供了不同的同步技术实现内存栅栏,例如sync.WaitGroup, 它保证了wg.Add和wg.Wait之间的happens-before关系。

现在回到本文最开始的例子,主要有两种方法修复它存在的问题。一种处理方法如下,在循环之前调用wg.Add操作。

代码语言:javascript
复制
wg := sync.WaitGroup{}
var v uint64

wg.Add(3)
for i := 0; i < 3; i++ {
        go func() {
                // ...
        }()
}

// ...

另一种方法是在每个循环的内部,但在启动子goroutine之前调用wg.Add操作,代码如下。

代码语言:javascript
复制
wg := sync.WaitGroup{}
var v uint64

for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
                // ...
        }()
}

// ...

上面的两种处理方法都是正确的。如果我们提前知道最终要设置的计数器的值是多少,那使用第一种处理方法可以只调用一次wg.Add,不像第二种处理方法要多次调用wg.Add操作,但有一点需要注意,后续操作等待的操作次数要与wg.Add添加的相同,这样可以避免一些细微的错误,例如wg.Add的值为3,但是后续等待的操作次数为2,这会导致永久阻塞。

总结,我们在编程时要小心别犯本文讨论的这个常见错误。在使用sync.WaitGroup时,Add操作必须在启动子goroutine之前,在父goroutine中执行完成,而Done操作必须在子goroutine内部执行完成。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 误用sync.WaitGroup
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档