Web Dev

Concurrency in Go Part II

This article is the last part in a two-part series about concurrency in Go. Check out the first part here.

In Part I of this series, we covered the basics of concurrency in Go: the differences between concurrency and parallelism, and how to implement goroutines, channels, and buffered channels in Go. Armed with those building blocks, we can work on some more sophisticated concurrency patterns by introducing synchronization.

Why Is Synchronization Needed?

If your program implements many goroutines, it’s likely that those goroutines will depend on information from one another. Since their execution order can’t be predicted in most cases, we need to introduce some mechanism for those goroutines to communicate with one another.

In the previous post, we learned how to read and write from a channel, which is a great way to pass values from one goroutine to the other. But coordinating execution of a high number of interdependent goroutines can get a bit tricky, so we need to use some new concepts.

Synchronization refers to the concept of adding an element of control over code execution. This includes both mechanisms without communication — like preventing two goroutines from writing to the same slice at the same time — and to mechanisms with communication, like waiting for a number of goroutines to finish before moving to the next step in a program.

Mutexes

Sometimes it’s necessary to simply prevent more than one goroutine from accessing a variable at a given time. This is called mutual exclusion, which is where the conventional name mutex originates. Mutexes are generally used by computer science professors to torture students into writing strange programs to simulate subway systems in major cities, but when writing Go and using goroutines, they serve an important purpose.

For example, given a block of code that updates some variable, it’s necessary to lock the read/write parts of the code so that only one goroutine is able to access it. When it’s safe again, we can unlock the code so that other goroutines can access it. Without the use of mutexes in concurrent code, things can get out of sync very quickly (though thankfully, no real trains will crash).

If you’re unfamiliar with the concept behind mutexes, here’s a quick example.

go func() {
  sum := 0
  for {
    mutex.Lock()
    sum += rand.Intn(15)
    mutex.Unlock()
  }
 }()

By using the mutex, we prevent multiple goroutines from updating the sum at the same time. This ensures that the value for sum stays accurate. At its base, the concept of a mutex isn’t incredibly complicated, but failing to use them when necessary can get you into trouble really quickly.

One other thing worth noting is that in Go, two different goroutines can be responsible for the lock and unlock of a mutex. This seems a bit strange, but it is allowed.

In addition to the basic mutex, golang also offers a RWMutex, which helps you control what types of access should be blocked. It’s often not necessary to prevent simultaneous reads of a value, but it is necessary to restrict the number of writers. By using a RWMutex instead of a normal mutex, you can still have the function or variable available for read operations, instead of locking it under any circumstance.

Synchronizing a Goroutine Group with a WaitGroup

WaitGroups have one purpose, and one purpose only: They wait. Specifically, they wait for a group of goroutines to finish.

A common situation is as follows: The main goroutine will call Add to the WaitGroup, which signifies how many goroutines must finish before it stops waiting. Then, each of the goroutines will call Done when it finishes, which decrements the WaitGroup counter. As long as the counter has a positive value, it will block. When the counter becomes 0, all blocked routines are released. But be careful in your implementation — a negative counter will panic, so take care that you signal Wait and Done appropriately.

For example, if you know that a certain goroutine will be called seven times, the main goroutine should add 7 to the waitgroup like this:

func someRoutine(s string, wg *sync.WaitGroup) {
    fmt.Println(“Hey! Did you know the value is %s?”, s)
    wg.Done()
}

func main() { 
  var wg sync.WaitGroup
  var n int = 7
  wg.Add(n)
  
  for i:=0; i < n; i++ {
go someRoutine(“someValue”, &wg)
  }

  wg.Wait() // this waits for the counter to be 0
  fmt.Println(“All finished!”)
)

This is preferable to calling wg.Add(1) each time the goroutine is called, because we already know how many times we expect the goroutine to run. Additionally, you could call wg.Add(1) at the start of the goroutine from inside the goroutine, but there is a danger that by the time your wg.Wait() is called, the counter will be at 0.

Notice that in the someRoutine function, we call wg.Done() right before the routine exits. If the code were more complex, we could take advantage of defer here, and instead defer wg.Done() until after someRoutine had completed.

func someRoutine(s string, wg *sync.WaitGroup) {
defer   wg.Done()
    // something really complex
}

Go makes it easy to write concurrent programs, but it won’t prevent race conditions automatically. You need to be diligent to prevent them and make sure your code is tested thoroughly. Race conditions are notorious for being really sporadic; it’s much easier to be meticulous during the writing process than to try to deal with buggy code in production resulting from a super-stealthy race condition.

Further Practice

If you aren’t familiar with Go By Example, I highly recommend checking it out. There are a few examples related to topics in this post series, namely goroutines and mutexes. The documentation on the sync package is also really solid.

And if you’re looking for more practice with Go in general, I super thumbs-up recommend working through some exercises on exercism.io. And if you want to level up, check out Brendan Fosberry’s post on using libchan to connect components of an application.

Reference: Concurrency in Go Part II from our WCG partner Florian Motlik at the Codeship Blog blog.

Laura Frank

Laura Frank is an engineer at Codeship. She loves containers, Docker, Golang, Ruby, and beer, not necessarily in that order.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button