注意1:使用指针
- 当把WaitGroup作为参数传递给函数时,如果传递的是变量本身(值传递),会发生复制。在 Go 语言中,这种复制可能会导致意外的行为。因为每个WaitGroup副本都有自己独立的计数器。
- 下面的代码如果worker(wg)这里没有传递指针,那么worker函数接收到的是wg的一个副本。worker函数中的Done操作是在副本上进行的,而main函数中的Wait操作是在原始的wg上进行的。这样就会导致main函数中的Wait可能永远阻塞,因为原始wg的计数器没有被正确地减少。
package mainimport ("fmt""sync")func worker(wg *sync.WaitGroup) {defer wg.Done()fmt.Println("Worker is running")}func main() {var wg sync.WaitGroupwg.Add(1)// 传递指针worker(&wg)wg.Wait()}
注意2:正确的计数器操作顺序
这一点十分容易出现很隐蔽的错误
-
Add方法调用时机:必须在goroutine 启动之前调用Add方法来增加计数器的值。如果在goroutine已经启动之后再调用Add,可能会导致Wait方法提前返回,因为计数器没有正确反映正在运行的goroutine的数量。把握住:Add()与Wait()保证在同一个函数中
var wg sync.WaitGroup// 错误示例,在goroutine启动后才调用Addgo func() {wg.Add(1)fmt.Println("Goroutine is running")wg.Done()}()wg.Wait()
下面是一个更加隐蔽的例子:
func TestDistribute(t *testing.T) {numOfTask := 5resChan := make(chan int, numOfTask)addTaskChan := make(chan *AddTask, numOfTask)var wg sync.WaitGroupgo initTask(addTaskChan, numOfTask, 11, resChan)go DistributeTasks(addTaskChan, numOfTask, &wg) // 没有在开启协程之前Add,开启之后在内部Add,就容易出问题res := 0wg.Wait()----------------------------------- func DistributeTasks(taskChan <-chan *AddTask, numOfTasks int, wg *sync.WaitGroup) {for task := range taskChan {wg.Add(1) // 在下面这个协程Add,只能保证在这个位置进行wg.Wait(),外面的函数无法保证go func(t *AddTask) {defer wg.Done()t.Do()}(task) // 注意要当作参数传入,而不是直接在 开启的协程 内部调用task,// wg.Wait() 在这个地方有效,外部就不行了} }
注意3:var wg sync.WaitGroup与wg := sync.WaitGroup{}区别
- var wg sync.WaitGroup
这种写法声明了一个 sync.WaitGroup 类型的变量 wg,但是它没有进行初始化,默认情况下是零值初始化。Go 中的零值初始化意味着 wg 会被自动初始化为 sync.WaitGroup{},即一个空的 WaitGroup,你可以直接使用它。 - wg := sync.WaitGroup{}
这种写法是对 sync.WaitGroup 的显式初始化,并且是局部变量的声明方式。 var wg sync.WaitGroup 的效果,初始化了一个新的空的 WaitGroup 变量。 - 使用 new(sync.WaitGroup) ,注意这里返回的是一个指针,这个时候回到第一点,传递wg的时候可以不需要使用 &wg取地址操作