Synchronized goroutines (part I)

Michał Łowicki
Nov 11, 2016 · 3 min read

Suppose that Go program starts two goroutines:

package mainimport (
"fmt"
"sync"
)
func main() {
var v int
var wg sync.WaitGroup
wg.Add(2)
go func() {
v = 1
wg.Done()
}()
go func() {
fmt.Println(v)
wg.Done()
}()
wg.Wait()
}

Both goroutines operates on shared variable v. One of them sets new value (writes) and the second one prints that variable (reads).

WaitGroup from sync package is used to wait till two non-main goroutines terminate. Otherwise there wouldn’t any guarantee that any of those goroutines would even start.

Since goroutines are independent concurrent tasks, there is no implicit ordering between operations they run. In the above example it‘s not clear if 0or 1will be printed. Output will be 1 when at the moment fmt.Println is fired, the other gorutine already executed assignment statement v = 0. It’s unknown though until the program will actually run. It other words assignment statement and call to fmt.Println aren’t ordered — they‘re concurrent.

It’s not good if we can’t tell what program does by looking at its source code. Go’s specification introduces partial order (happens before) of the memory operations (reads or writes). This order enables to deduce what program does. Certain mechanisms in the language additionally allow programer to enforce order of operations.

Inside single goroutine all operations are ordered as they’re placed in the source code:

wg.Add(2)
wg.Wait()

Function calls from the above example since placed inside the same goroutine are ordered — wg.Add(2) happens before wg.Wait().

1. Channels

Communication using channels is the primary method for synchronization. Sending value to a channel happens before receiving it:

var v int
var wg sync.WaitGroup
wg.Add(2)
ch := make(chan int)
go func() {
v = 1
ch <- 1
wg.Done()
}()
go func() {
<-ch
fmt.Println(v)
wg.Done()
}()
wg.Wait()

The new thing is ch channel. Since receiving happens after sending value to a channel and sending value happens after assigning to v then above program prints always 1:

set v → send to ch → receive from ch → print v

First and third arrow are consequences of ordering within the same goroutine. Using channel communication introduced second arrow. Ultimately operations spread across two goroutines are ordered.

2. sync package

Package sync provides synchronization primitives. One of them which could solve our problem is Mutex. Having a variable lock of type sync.Mutex it’s guaranteed that 2nd call to lock.Lock() happens after 1st call to lock.Unlock(). 3rd call to lock.Lock() happens after 1st and 2nd calls to lock.Unlock(). Generally speaking nth call to lock.Lock() happens after mth call to lock.Unlock() for m < n. Let’s see how to use this knowledge for our synchronization problem:

var v int
var wg sync.WaitGroup
wg.Add(2)
var m sync.Mutex
m.Lock()
go func() {
v = 1
m.Unlock()
wg.Done()
}()
go func() {
m.Lock()
fmt.Println(v)
wg.Done()
}()
wg.Wait()

In future stories more will be shown about communication with channels (i.e. how it works with buffered channels) but also what is provided by sync package will be explained in details.

Click ❤ below to help others discover this story. If you want to get updates about new posts please follow me.

Resources

golangspec

A series dedicated to deeply understand Go’s specification and language’s nuances

Michał Łowicki

Written by

Software engineering manager at Facebook, previously Opera, never satisfied.

golangspec

A series dedicated to deeply understand Go’s specification and language’s nuances

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade