Mutex locks in Golang

Mutex locks in Golang

While writing concurrent code in golang, often programs face situations when two or more goroutines try to access and modify shared variables. Such conditions can lead to inconsistency in data stored in the shared variable. Such conditions are called Race conditions. You can learn more about race conditions from our previous article of this course, Race conditions in Golang.

Let's see one example of Race Condition:

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func main() {

    var counter int
    fmt.Println("Initial value: ", counter)

    // deploy 5 goroutines
    for i := 0; i < 5; i++ {

        wg.Add(1)

        go func() {
            defer wg.Done()
            //increment the counter 100 times
            for j := 0; j < 100; j++ {
                temp := counter
                time.Sleep(time.Microsecond * 1)
                temp += 1
                counter = temp
            }

        }()

    }

    wg.Wait()
    fmt.Println("Final value: ", counter)
}

Output

Initial value:  0
Final value:  102

As we can see in the output, there's definitely an inconsistency in our counter variable. The expected value of the counter is 500 and we go the 102. Let's find out where is the issue in the above code causing the Race condition. To find the race condition in our code, we can use the command go run -race main.go.

When we run/build our code with the -race flag on. It generates the following output.

Initial value:  0
==================
WARNING: DATA RACE
Read at 0x00c000126008 by goroutine 8:
  main.main.func1()
      ioScript/concurrency/main.go:25 +0xa8

Previous write at 0x00c000126008 by goroutine 7:
  main.main.func1()
      ioScript/concurrency/main.go:28 +0xce

Goroutine 8 (running) created at:
  main.main()
     ioScript/concurrency/main.go:21 +0xdb

Goroutine 7 (running) created at:
  main.main()
      ioScript/concurrency/main.go:21 +0xdb
==================
Final value:  101
Found 1 data race(s)
exit status 66

We can see in the logs that go runtime found 1 data race. A race condition was found due to lines 25 and 28. There is a read operation in line 25 and a write operation at 28. Goroutine 8 is reading from the counter variable and Goroutine 7 is trying to write to the counter variable.

We can solve this race condition if we somehow restrict the read and write operation of the shared variable to one goroutine at a time.

sync.Mutex

Mutex is a synchronization primitive which restricts one or more goroutine to enter the critical section while another goroutine is executing the critical section.

When one goroutine is executing the critical section of the code, mutex locks the section. Other goroutines need to wait till the execution is complete and the lock is released by the first goroutine.

Let's try to fix our above code using mutex lock.

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg sync.WaitGroup

func main() {

    var counter int
    var mu sync.Mutex

    fmt.Println("Initial value: ", counter)

    // deploy 5 goroutines
    for i := 0; i < 5; i++ {

        wg.Add(1)

        go func() {
            defer wg.Done()
            //increment the counter 100 times
            for j := 0; j < 100; j++ {

                mu.Lock()
                temp := counter
                time.Sleep(time.Microsecond * 1)
                temp += 1
                counter = temp
                mu.Unlock()
            }

        }()

    }

    wg.Wait()
    fmt.Println("Final value: ", counter)
}

Output

ioscript@ioscript:concurrency$  go run -race main.go 
Initial value:  0
Final value:  500

So, let's break down what we have done here. We have declared a variable name mu which is of type Mutex provided by the sync package of golang. Mutex type provides two methods: Lock() and Unlock().

  • Lock( ): As the name suggests, it locks the mutex. If the lock is already in use, other goroutine blocks until the mutex is available.
  • Unlock( ): It unlocks the mutex. It throws a runtime error if the mutex is not locked when unlock was called.

In our code above, we have used mu.Lock() just before we are reading a value from the counter variable. Then we are incrementing the value and write the value to the counter variable again. Since read-write operations are complete, we are releasing the lock on mutex using mu.Unlock( ) so that other goroutines can acquire the lock again and execute the critical section.

When we run this code with the -race flag, we don't get any error message and the correct output is displayed in the code.

Conclusion

In this tutorial, we had a quick look into Race conditions and how they can create data inconsistency in our program. We then learn about Mutex Locks and how we can use them to solve race conditions in our code and ensure that our program always returns expected results.

Before You Leave

If you found this article valuable, you can support us by dropping a like and sharing this article with your friends. You can find more articles in this series here

You can sign up for our newsletter to get notified whenever we post awesome content on Golang.

You can also let us know what you would like to read next? Drop a comment or email @

Reference

Frame 1.png

Did you find this article valuable?

Support ioScript.org by becoming a sponsor. Any amount is appreciated!