How to use synchronization primitives in Go: Mutex, WaitGroup, Once

abhirockzz

Abhishek Gupta

Posted on October 14, 2019

How to use synchronization primitives in Go: Mutex, WaitGroup, Once

Welcome to Just Enough Go ! This is the second post in a series of articles about the Go programming language in which I will be covering some of the most commonly used Go standard library packages e.g. encoding/json, io, net/http, sync etc. I plan to keep these relatively short and example driven.

Let's look at some of the lower-level synchronization constructs which Go provides in the sync package, in addition to goroutines and channels. There are a bunch of them, but we will explore WaitGroup, Mutex and Once with examples.

Code examples are available on GitHub

WaitGroup

Use a WaitGroup for co-ordination if your program needs to wait for a bunch of goroutines to finish. It is similar to a CountDownLatch in Java. Let's see an example.

We want to print all the files in our home directory in parallel. Use a WaitGroup to specify the number of tasks/goroutines to wait for - in this case, it is the same as the number of files/directories you have in the home directory. We use Wait() to block until the WaitGroup counter becomes zero.

...
func main() {
    homeDir, err := os.UserHomeDir()
    if err != nil {
        panic(err)
    }
    filesInHomeDir, err := ioutil.ReadDir(homeDir)
    if err != nil {
        panic(err)
    }
    var wg sync.WaitGroup
    wg.Add(len(filesInHomeDir))
    for _, file := range filesInHomeDir {
        go func(f os.FileInfo) {
            defer wg.Done()
        }(file)
    }
    wg.Wait()
}
...
Enter fullscreen mode Exit fullscreen mode

To run this program:

curl https://raw.githubusercontent.com/abhirockzz/just-enough-go/master/sync/wait-group-example.go -o wait-group-example.go
go run wait-group-example.go
Enter fullscreen mode Exit fullscreen mode

A goroutine is spawned for each os.FileInfo we find in the user home directory and once we print its name, the counter is decremented using Done. The program exits after all the contents of the home directory are covered.

Mutex

A Mutex is a shared lock which you can use to provide exclusive access to certain parts of your code. In this simple example, we have a shared/global variable accessCount which is used in the incr function.

func incr() {
    mu.Lock()
    defer mu.Unlock()
    accessCount = accessCount + 1
}
Enter fullscreen mode Exit fullscreen mode

Notice that the incr function is protected by a Mutex. Thus, only a single goroutine can access it at a time. We throw mulitple goroutines at it

loop := 500
for i := 1; i <= loop; i++ {
        go func(c int) {
            wg.Add(1)
            defer wg.Done()
            incr()
        }(i)
}
Enter fullscreen mode Exit fullscreen mode

If you run this, you will always get the same result i.e. Final = 500 (since the for loop runs for 500 iterations). To run the program:

curl https://raw.githubusercontent.com/abhirockzz/just-enough-go/master/sync/mutex-example.go -o mutex-example.go
go run mutex-example.go
Enter fullscreen mode Exit fullscreen mode

Comment (or remove) the following lines in the incr function and run the program on your local machine using and run the program again

mu.Lock()
defer mu.Unlock()
Enter fullscreen mode Exit fullscreen mode

You will notice variable results e.g. Final = 474

I encourage you to read up on RWMutex. It is a special kind of lock which can be used to allow concurrent reads but synchronized (single writer) writes.

Once

It allows you to define a task which you only want to execute once during the lifetime of your program. This is very useful for Singleton-like behavior. It has a single Do function which let's you pass another function which you intend to execute only once. Let's look at an example

Say you're building a REST API using the Go net/http package and you want some piece of code to be executed only when the HTTP handler is called (e.g. a get a DB connection). You can wrap that code with once.Do and rest assured that it will be only run when the handler is invoked for the first time.

Here is a function which we want to be executed only once

func oneTimeOp() {
    fmt.Println("one time op start")
    time.Sleep(3 * time.Second)
    fmt.Println("one time op started")
}
Enter fullscreen mode Exit fullscreen mode

This is what we do within our HTTP handler - notice once.Do(oneTimeOp)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Println("http handler start")
        once.Do(oneTimeOp)
        fmt.Println("http handler end")
        w.Write([]byte("done!"))
    })
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Enter fullscreen mode Exit fullscreen mode

Run the code and access the REST endpoint

curl https://raw.githubusercontent.com/abhirockzz/just-enough-go/master/sync/once-example.go -o once-example.go
go run once-example.go
Enter fullscreen mode Exit fullscreen mode

From a different terminal

curl localhost:8080
//output - done!
Enter fullscreen mode Exit fullscreen mode

When you first access it, it will be a little slow in returning and you will see the following logs in the server:

http handler start
one time op start
one time op end
http handler end
Enter fullscreen mode Exit fullscreen mode

If you run it again (any number of times), the function oneTimeOp will not be executed. Check the logs to confirm

That's all for this blog. I would be more than happy to take suggestions on specific Go topics which you would like to me cover! Feel free to tweet or just drop a comment and don't forget to like and follow 😃😃

💖 💪 🙅 🚩
abhirockzz
Abhishek Gupta

Posted on October 14, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related