本文介绍协同同步机制,基于示例和源码详解其实现原理,并总结应用同步机制的最佳实践。

问题引入

为了等待协程完成,我们可以使用空结构体通道,并在操作最后给通道发送值。

	ch := make(chan struct{})
	for i := 0; i < n; n++ {
		go func() {
			// do something
			ch <- struct{}{}
		}()
	}
	for i := 0; i < n; n++ {
		<-ch
	}

这种策略可以实现,但不建议使用。这在语义上也不正确,使用通道作为工具发送空数据,我们的使用场景是同步而不是通信。这就需要引入sync.WaitGroup 数据结构,专门用于同步场景。

同步机制

sync.WaitGroup 数据结构包括主状态,称为计数器,标识等待元素数量,源码如下:

// A WaitGroup waits for a collection of goroutines to finish.
// The main goroutine calls Add to set the number of
// goroutines to wait for. Then each of the goroutines
// runs and calls Done when finished. At the same time,
// Wait can be used to block until all goroutines have finished.
//
// A WaitGroup must not be copied after first use.
//
// In the terminology of the Go memory model, a call to Done
// “synchronizes before” the return of any Wait call that it unblocks.
type WaitGroup struct {
	noCopy noCopy

	// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
	// 64-bit atomic operations require 64-bit alignment, but 32-bit
	// compilers only guarantee that 64-bit fields are 32-bit aligned.
	// For this reason on 32 bit architectures we need to check in state()
	// if state1 is aligned or not, and dynamically "swap" the field order if
	// needed.
	state1 uint64
	state2 uint32
}

noCopy字段防止按值拷贝,还有状态字段。另外还提供三个方法:

  • Add

使用指定值改变计数器的值,也可以是负数。如果计数器变为零,应用会panics.

  • Done
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
	wg.Add(-1)
}

可以但Done是Add(-1)的简化形式。通常在协程完成工作时调用。

  • Wait

该操作阻塞当前协程直到计数器为零。

案例分析

下面使用WaitGroup重构上面示例,会让代码更清晰、易读:

	func main() {
		wg := sync.WaitGroup{}
		wg.Add(10)
		for i := 1; i <= 10; i++ {
			go func(a int) {
				for i := 1; i <= 10; i++ {
					fmt.Printf("%dx%d=%d\n", a, i, a*i)
				}
				wg.Done()
			}(i)
		}
		wg.Wait()
	}
}

循环开始前,我们设置WaitGroup计数器为协程的数量,这我们在启动之前就已知道。然后每完成一个使用Done方法减少计数器。

如果事前不知道总数量呢,Add方法可以在开始执行协程之前增加1,代码如下:

func main() {
	wg := sync.WaitGroup{}
	for i := 1; rand.Intn(10) != 0; i++ {
		wg.Add(1)
		go func(a int) {
			for i := 1; i <= 10; i++ {
				fmt.Printf("%dx%d=%d\n", a, i, a*i)
			}
			wg.Done()
		}(i)
	}
	wg.Wait()
}

上面示例事前不任务数量不确定,因此在每次任务之前调用Add(1),其他过程与上面一致。

常见的错误是add方法在协程内部,这通常会导致提前退出,而不执行任何gor协程。

引用源码中的注释作为最佳实践:

A WaitGroup waits for a collection of goroutines to finish.

The main goroutine calls Add to set the number of goroutines to wait for. 
Then each of the goroutines runs and calls Done when finished. 
At the same time, Wait can be used to block until all goroutines have finished.

简单翻译下:

WaitGroup实现等待一组协程完成机制。

  • 主协程调用Add方法,并使之需要等待协程的数量
  • 每个协程运行,结束时调用Done方法
  • 同时,Wait方法阻塞,直到所有协程都完成