Performance gains with goroutines

Waiting for Stragglers

Extending the length of the Sleep command in the main program and hoping for good luck that all the underlings have finished their work in the meantime is obviously not a good solution. If the computer is busy with costly operations in other processes in the meantime, it is possible that the duration will extend to a few seconds, and the race is on again.

For every goroutine to have a guaranteed outcome, the main program and the routines have to communicate. At the end of the program flow, the main program has to wait until each routine has successfully completed before it can shut down the main process. In the form of the sync package, Go offers some tools based on semaphores that do this job reliably.

The most elegant method, preferred by Go programmers, however, uses channels. These communication lines, reminiscent of Unix pipes, transport information from one part of the program to another. In addition, they block the program flow in a routine if nothing can be fed into the channel or read out from it temporarily and are thus ideal for synchronization, because individual program parts can wait for each other.

Channels, Synchronize!

Listing 4 fires off three different goroutines again, but doesn't output anything directly in them. Instead, the goroutines feed their output, which contains data of the string type, into a channel named done defined in line 5. The inverted arrow <- pointing from the data to be written (e.g., "a") to the channel sends the data into the channel (Figure 3).

Listing 4


01 package main
02 import "fmt"
04 func main() {
05   done := make(chan string)
07   go func() { done <- "a" }()
08   go func() { done <- "b" }()
09   go func() { done <- "c" }()
11   defer close(done)
13   for i := 0; i <= 2; i++ {
14     msg := <-done
15     fmt.Println(msg)
16   }
17 }
Figure 3: The Go syntax for write and read channel access takes some getting used to.

Depending on the free capacity in the channel, this is done either immediately, or the Go runtime blocks the execution of the respective goroutine until the channel can receive the data. It is very important that while one goroutine might block at any given time, other goroutines in the system continue to run unhindered and thus do not cause a hiccup in the program, but instead empower a high-performance system.

The output from Listing 4 is also non-deterministic, whether you will see abc or cba or bac is uncertain, because the single goroutines in this simple implementation don't coordinate their work with each other, and the main program only waits until all routines are finished – the order thus is rather random. What is guaranteed, though, is that the output will always contain three letters, which is an improvement over the previous race condition.

After firing off the goroutines, the main program uses the defer statement in line 11 to stipulate that the done channel will be closed after the program terminates; it then enters a for loop, which uses the read operator <- on the left (!) side of the channel variable to fetch the next value that exists in the channel in line 14 and assigns it to the msg variable.

Synchronization with the previously spawned goroutines also occurs during this read operation. When the main program reaches the for loop and the read operation for the first time, it is highly unlikely that any of the goroutines have had an opportunity to execute their write statements thus far. But this doesn't matter; if the channel is still empty, the program blocks in line 14 until data becomes available and the Go runtime lets one of the goroutines loose to perform its task. As soon as the first chunk of data has trickled in, the blocked read statement in the main program also notices that things are happening, obtains the available data, and the for loop goes into the next round.

Precise Count

Now it becomes clear why, in this simple implementation, the for loop in line 13 needs to know exactly how many data packages were fed into the channel by the goroutines in order to retrieve precisely that number. If the main program were simply to continue to ask the channel for data, the Go runtime would block the fourth read process for an infinite period of time, because no further data would be written to the channel from this point onward. A hanging main program would be the result.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Rat Rac

    If program parts running in parallel keep interfering with each other, you may have a race condition. Mike Schilli shows how to instruct the Go compiler to detect these conditions and how to avoid them in the first place.

  • Let's Go!

    Released back in 2012, Go flew under the radar for a long time until showcase projects such as Docker pushed its popularity. Today, Go has become the language of choice of many system programmers.

  • Fighting Chaos

    When functions generate legions of goroutines to do subtasks, the main program needs to keep track and retain control of ongoing activity. To do this, Mike Schilli recommends using a Context construct.

  • Motion Sensor

    Inotify lets applications subscribe to change notifications in the filesystem. Mike Schilli uses the cross-platform fsnotify library to instruct a Go program to detect what's happening.

  • Progress by Installments

    Desktop applications, websites, and even command-line tools routinely display progress bars to keep impatient users patient during time-consuming actions. Mike Schilli shows several programming approaches for handwritten tools.

comments powered by Disqus

Direct Download

Read full article as PDF:

Price $2.95