Go并发:使用sync.WaitGroup实现协程同步方式

时间:2022-11-26 19:36:01

经常看到有人会问如何等待主协程中创建的协程执行完毕之后再结束主协程,例如如下代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
    "fmt"
)
func main() {
    go func() {
        fmt.Println("Goroutine 1")
    }()
    go func() {
        fmt.Println("Goroutine 2")
    }()
}

执行以上代码很可能看不到输出,因为有可能这两个协程还没得到执行主协程已经结束了,而主协程结束时会结束所有其他协程。

解决办法是可以在main函数结尾加上等待:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
    "fmt"
    "time"
)
func main() {
    go func() {
        fmt.Println("Goroutine 1")
    }()
    go func() {
        fmt.Println("Goroutine 2")
    }()
    time.Sleep(time.Second * 1) // 睡眠1秒,等待上面两个协程结束
}

这并不是完美的解决方法,如果这两个协程中包含复杂的操作,可能很耗时间,就无法确定需要睡眠多久,当然可以用管道实现同步:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import (
    "fmt"
)
func main() {
    ch := make(chan struct{})
    count := 2 // count 表示活动的协程个数
    go func() {
        fmt.Println("Goroutine 1")
        ch <- struct{}{} // 协程结束,发出信号
    }()
    go func() {
        fmt.Println("Goroutine 2")
        ch <- struct{}{} // 协程结束,发出信号
    }()
    for range ch {
        // 每次从ch中接收数据,表明一个活动的协程结束
        count--
        // 当所有活动的协程都结束时,关闭管道
        if count == 0 {
            close(ch)
        }
    }
}

上面的解决方案是比较完美的方案,但是Go提供了更简单的方法——使用sync.WaitGroup。

WaitGroup顾名思义,就是用来等待一组操作完成的。

WaitGroup内部实现了一个计数器,用来记录未完成的操作个数,它提供了三个方法,Add()用来添加计数。

Done()用来在操作结束时调用,使计数减一。

Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import (
    "fmt"
    "sync"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(2) // 因为有两个动作,所以增加2个计数
    go func() {
        fmt.Println("Goroutine 1")
        wg.Done() // 操作完成,减少一个计数
    }()
    go func() {
        fmt.Println("Goroutine 2")
        wg.Done() // 操作完成,减少一个计数
    }()
    wg.Wait() // 等待,直到计数为0
}

可见用sync.WaitGroup是最简单的方式。

补充:Golang 中使用WaitGroup的那点坑

sync.WaitGroup对于Golang开发者来说并不陌生,其经常作为多协程之间同步的一种机制。用好它势必会让你事半功倍,但是一旦错用将引发问题。

关于WaitGroup的使用网上有很多例子,在此就不做介绍了,我想说的是我在项目中使用WaitGroup遇到的坑。

在项目中,因为服务器有同步需求, 所以直接使用了WaitGroup,但是未考虑使用场景,结果在项目上线之后,高峰期的时候客户端经常出现卡顿,经过多方查找,才发现如果使用WaitGroup的时候,未启动单独的goroutine,那么极有可能造成主线程的阻塞,

所以我做了下面的测试(测试中,我把WaitGroup置于协程内):
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import (
 "fmt"
 "sync"
 "time"
)
 
func main() {
    fmt.Println("main-1")
 testW()
 fmt.Println("main-2")
 time.Sleep(time.Duration(15) * time.Second)
}
 
func testW() {
 fmt.Println("testW-1")
 go func() {
  var wg sync.WaitGroup
  fmt.Println("testW-2")
  testW1(&wg)
  fmt.Println("testW-5")
  wg.Wait()
  fmt.Println("testW-6")
 }()
}
 
func testW1(wg *sync.WaitGroup) {
 wg.Add(1)
 fmt.Println("testW-3")
 time.AfterFunc(time.Second*5, func() {
  wg.Done()
 })
 fmt.Println("testW-4")
}

输出为:

main-1

testchan-1

main-2

testchan-2

testchan-3

testchan-4

testchan-5

// 过5秒

testchan-6

总结:

将WaitGroup用于goroutine内,不会导致主线程的阻塞,同样可以实现同步的效果。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持服务器之家。如有错误或未考虑完全的地方,望不吝赐教。

原文链接:https://blog.csdn.net/u011304970/article/details/72722044