Synchronizing goroutines

Until now, in order to wait for goroutines to finish, we used a channel of empty structures and sent a value through the channel as the last operation, as follows:

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

This strategy works, but it's not the preferred way to accomplish the task. It's not correct semantically, because we are using a channel, which is a tool for communication, to send empty data. This use case is about synchronization rather than communication. That's why there is the sync.WaitGroup data structure, which covers such cases. It has a main status, called counter, which represents the number of elements waiting:

type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}

The noCopy field prevents the structure from being copied by value with panic. The state is an array made by three int32, but only the first and last entries are used; the remaining one is used for compiler optimizations.

The WaitGroup offers three methods to accomplish the same result:

  • Add: This changes the value of the counter using the given value, which could also be negative. If the counter goes under zero, the application panics.
  • Done: This is a shorthand for Add with -1 as the argument. It is usually called when a goroutine finishes its job to decrement the counter by 1.
  • Wait: This operation blocks the current goroutine until the counter reaches zero.

Using the wait group results in a much cleaner and more readable code, as we can see in the following example:

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 ", a, i, a*i)
}
wg.Done()
}(i)
}
wg.Wait()
}

To the wait group, we are adding a delta equal to goroutines, which we will launch beforehand. In each single goroutine, we are using the Done method to reduce the count. If the number of goroutines is not known, the Add operation (with 1 as its argument) can be executed before starting each goroutine, as shown in the following:

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 ", a, i, a*i)
}
wg.Done()
}(i)
}
wg.Wait()
}

In the preceding example, we have a 10% chance of finishing each iteration of the for loop, so we are adding one to the group before starting the goroutine.

A very common error is to add the value inside the goroutine, which usually results in a premature exit without any goroutines executed. This happens because the application creates the goroutines and executes the Wait function before the routines start and add their own delta, as in the following example:

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

This application will not print anything because it arrives at the Wait statement before any goroutine is started and the Add method is called.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.103.219