前言
先来看这样一段计数的程序:
var counter int
var mu sync.Mutex
func increment() {
mu.Lock()
counter++
defer mu.Unlock()
}
func main() {
for i := 0; i < 100; i++ {
go func() {
increment()
}()
}
fmt.Println("Counter:", counter)
}
如果你执行了该程序,发现结果值小于100,这是为啥呢?
这段代码中, main 函数启动了 100 个 goroutine ,并行执行 increment 函数, 每个 goroutine 都会对 counter 进行加锁和自增操作。然而, main 函数在所有 goroutine 执行完之前就调用了 fmt.Println("Counter:", counter), 这时打印出来的 counter 值可能是一个较小的值,因为并发执行的 goroutine 可能还没有完成。如何解决呢?
Go 语言提供了许多并发工具来帮助开发者管理多个 goroutine 的生命周期和同步。 sync.WaitGroup 和 sync.Once 是其中两个非常常用且重要的工具,它们分别用于等待一组 goroutine 完成和确保某个操作只执行一次。
sync.WaitGroup
sync.WaitGroup 用于等待多个 goroutine 完成。 通过 Add、 Done 和 Wait 方法, sync.WaitGroup 可以确保我们能够在主线程或者其他 goroutine 中等待多个并发任务完成。
常用方法
Add(delta int):增加或减少等待计数器。delta参数可以是正数或负数,通常使用Add(1)来表示有一个 goroutine 需要等待,Done()方法内部会调用Add(-1)来表示 goroutine 完成。Done():表示当前 goroutine 完成。当一个 goroutine 执行完毕时调用Done(),减少WaitGroup的计数。Wait():阻塞当前 goroutine,直到所有 goroutine 完成。它会阻塞直到Add增加的数量通过Done调用相应次数。
2. 使用示例
还是前言中的案例,我们进行如下修改:
var counter int
var mu sync.Mutex
func increment() {
mu.Lock()
counter++
defer mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
这段代码,我们使用 wg.Add(1) 来告诉 WaitGroup 我们需要等待一个 goroutine, wg.Done() 用于每个 goroutine 完成时调用,减少计数器。最后, wg.Wait() 会阻塞,直到所有 goroutine 完成。
此时,再执行这段代码,输出结果就是100.
sync.Once
sync.Once 是一种机制,它确保某个操作只会执行一次,无论有多少 goroutine 尝试执行该操作。 sync.Once 对象的核心方法是 Do ,它会确保提供的函数只被执行一次,无论有多少个 goroutine 调用它。
常用方法
Do(f func()):如果f还没有执行过,它将执行一次。无论调用多少次,f函数只会被执行一次。
使用示例
var once sync.Once
var wg sync.WaitGroup
func initOnce() {
fmt.Println("Initializing...")
}
func worker(id int) {
defer wg.Done()
// 确保初始化函数只执行一次
once.Do(initOnce)
fmt.Printf("Worker %d is working\n", id)
}
func main() {
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i)
}
wg.Wait()
}
这段代码中,在 worker 函数中,使用了 sync.Once 来确保 initOnce 函数只被执行一次。 即使有多个 goroutine 调用 worker, initOnce 也仅会执行一次。
执行代码,输出如下:
Initializing...
Worker 1 is working
Worker 2 is working
Worker 3 is working
不管启动多少个 worker, initOnce 函数始终只会被调用一次,这就是 sync.Once 的强大之处。
sync.WaitGroup 和 sync.Once 对比
| 特性 | sync.WaitGroup | sync.Once |
|---|---|---|
| 主要用途 | 等待一组 goroutine 完成 | 确保某个操作只执行一次 |
| 适用场景 | 当你需要在多个并发操作完成后继续执行下一步工作时 | 当你需要初始化某些资源或做一次性工作时 |
| 常用方法 | Add、 Done、 Wait | Do |
| 使用限制 | 必须调用 Done 与 Add 配合使用 | 只执行一次,且没有回调次数限制 |
使用WaitGroup值实现一对多的 goroutine 协作流程时,怎样才能让分发子任务的 goroutine 获得各个子任务的具体执行结果
通常的做法是使用 通道(Channel) 来传递每个子任务的执行结果。通过这种方式,主 goroutine 可以从子任务 goroutine 中获取结果。
具体步骤如下:
- 创建一个用于传递结果的通道 :每个子任务的执行结果将通过这个通道传递给主 goroutine。
- 等待所有 goroutine 完成 :通过
WaitGroup等待所有子任务完成。 - 收集结果 :主 goroutine 通过通道收集来自子任务 goroutine 的结果。
这里假设我们有 5 个子任务,每个子任务都返回一个整数结果,主 goroutine 将等待所有子任务完成并收集它们的结果。
func processTask(id int, ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
result := id * id
ch <- result // 将结果发送到通道
}
func main() {
var wg sync.WaitGroup
resultCh := make(chan int, 5) // 创建一个缓冲区大小为 5 的通道,存储每个任务的结果
for i := 1; i <= 5; i++ {
wg.Add(1)
go processTask(i, resultCh, &wg)
}
wg.Wait()
close(resultCh) // 关闭通道,表示没有更多的数据发送
// 收集并打印结果
for result := range resultCh {
fmt.Println("Task result:", result)
}
}
这段代码中,我们使用一个带缓冲的通道 resultCh 来传递每个子任务的执行结果。通道的缓冲区大小为 5,因为我们计划启动 5 个子任务。 当所有 goroutine 完成后,我们关闭通道 resultCh ,表示没有更多的数据会被发送到该通道。关闭通道是为了避免在主 goroutine 中发生死锁。 主 goroutine 在 close(resultCh) 后,通过 range 从通道中读取结果。
执行代码,输出如下:
Task result: 4
Task result: 25
Task result: 1
Task result: 9
Task result: 16
最后
再总结一下, sync.WaitGroup 使我们能够等待多个 goroutine 完成工作,而 sync.Once 确保某个操作只会执行一次。理解并正确使用这些工具能够让你的并发程序更加高效、简洁和可靠。