前言
先来看这样一段计数的程序:
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
函数, 每个 goroutin
e 都会对 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
确保某个操作只会执行一次。理解并正确使用这些工具能够让你的并发程序更加高效、简洁和可靠。