跳到主要内容

同步2-等待组

一 等待组 sync.WaitGroup

sync.WaitGroup类型的值也是并发安全的,该类型结构体中内内部拥有一个计数器,计数器的值可以通过方法调用实现计数器的增加和减少 。

当我们添加了 N 个并发任务进行工作时,就将等待组的计数器值增加 N。每个任务完成时,这个值减1。 同时,在另外一个 goroutine 中等待这个等待组的计数器值为 0 时, 表示所有任务己经完成。

等待组常用方法:

  • (wg *WaitGroup) Add(delta int) 等待组计数器+1,该方法也可以传入负值让等待计数减少,切记不能减少为负数,会引发崩溃
  • (wg *WaitGroup) Done() 等待组计数器-1,等同于Add传入负值
  • (wg *WaitGroup) Wait() 等待组计数器!=0时阻塞,直到为0

应用场景:WaitGroup一般用于协调多个goroutine运行。

简单示例:

package main

import (
"fmt"
"net/http"
"sync"
)

func main() {

var wg sync.WaitGroup // 声明一个等待组

var urls = []string{
"https://www.baidu.com/",
"https://www.163.com/",
"https://www.weibo.com/",
}

for _, url := range urls {
wg.Add(1) // 每个任务开始,等待组+1
go func(url string) {
defer wg.Done()
_, err := http.Get(url) // 执行访问
fmt.Println(url, err)
}(url)
}

wg.Wait() // 等待所有任务完成
fmt.Println("over")

}

上述案例可以使用channel方式,每个go协程执行时,channel传递完成信号,但是使用通道的方式明显过重。

贴士:有了等待组,我们就不需要再在main函数中使用 time.Sleep()方法来模拟等待协程运行结束了。

二 等待组与锁配合使用示例

开启多个协程对共享内存产生竞争,单独使用等待组不能解决问题,如下所示:

package main

import (
"fmt"
"sync"
)

func main() {

var wg sync.WaitGroup
var money = 10000

// 开启10个协程,每个协程内部 循环1000次,每次循环值+10
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
for j := 0; j < 100; j++ {
money += 10 // 多个协程对 money产生了竞争
}
wg.Done()
}()
}

wg.Wait()


fmt.Println("最终的monet = ", money) // 应该输出20000才正确,但是每次运行输出结果不正确

}

等待组与互斥锁(同步锁)配合解决钱数问题示例:

package main

import (
"fmt"
"sync"
)

func main() {

var mt sync.Mutex
var wg sync.WaitGroup
var money = 10000

// 开启10个协程,每个协程内部 循环1000次,每次循环值+10
for i := 0; i < 10; i++ {
wg.Add(1)
go func(index int) {
mt.Lock()
fmt.Printf("协程 %d 抢到锁\n", index)
for j := 0; j < 100; j++ {
money += 10 // 多个协程对 money产生了竞争
}
fmt.Printf("协程 %d 准备解锁\n", index)
mt.Unlock()
wg.Done()
}(i)
}

wg.Wait()


fmt.Println("最终的monet = ", money) // 应该输出20000才正确

}

可能的打印结果:

协程 9 抢到锁
协程 9 准备解锁
协程 4 抢到锁
协程 4 准备解锁
协程 0 抢到锁
协程 0 准备解锁
协程 1 抢到锁
协程 1 准备解锁
协程 2 抢到锁
协程 2 准备解锁
协程 3 抢到锁
协程 3 准备解锁
协程 7 抢到锁
协程 7 准备解锁
协程 5 抢到锁
协程 5 准备解锁
协程 8 抢到锁
协程 8 准备解锁
协程 6 抢到锁
协程 6 准备解锁
最终的monet = 20000