DEV Community

loading...
Cover image for Mutexes and RWMutex in Golang

Mutexes and RWMutex in Golang

Cristian Curteanu
JS, Ruby and Go(lang) enthusiast
・3 min read

This article was originally published here

If you have ever used processing using multiple threads, you probably are fully aware about what are race conditions.

Just to recap, it happens when two processing threads are accessing the same value in memory, and are changing it. Because of non-deterministic nature of concurrent threads, the value stored will comply for only one specific thread, which leads to a inconsistent behaviour. If you are working on a payment system, or a banking software this can can cost business huge amount of money.

To avoid that, there is a mechanism in go for locking the value in the memory, called Mutext. This stands for mutual exclusion, and what it means is that it keeps track of which thread has access to specific value in memory, at specific point in time.

Let's consider the following piece of code:

type Account struct {
    balance int
    Name    string
}

func (a *Account) Withdraw(amount int) {
    a.balance -= amount
}

func (a *Account) Deposit(amount int) {
    a.balance += amount
}

func (a *Account) GetBalance() int {
    return a.balance
}
Enter fullscreen mode Exit fullscreen mode

It is a pretty simple structure in Go, that keeps track of accounts balance, nothing so special. However, if we will try to withdraw, or deposit an amount on some specific account from multiple goroutines, it will inevitably lead to a race condition, like this:

var account Account
account.Name = "Test account"

for i := 0; i < 20; i++ {
  wg.Add(1)
  go account.Deposit(100)
}

for i := 0; i < 10; i++ {
  wg.Add(1)
  go account.Withdraw(100)
}

wg.Wait()
fmt.Printf("Balance: %d\n", account.GetBalance())
Enter fullscreen mode Exit fullscreen mode

This means, that when running this code several times, the value of balance will be inconsistent, meaning it will have different values. In order to avoid that, we need to add locks for both reading and writing to the values. In our case, the Mutex lock will be added as a field to this structure, which Golang runtime will understand, that this lock is applied for the scope of this specific struct:

type Account struct {
    balance int
    Name    string
    lock    sync.Mutex
}

func (a *Account) Withdraw(amount int, wg *sync.WaitGroup) {
    defer wg.Done()
    a.lock.Lock()
    a.balance -= amount
    a.lock.Unlock()
}

func (a *Account) Deposit(amount int, wg *sync.WaitGroup) {
    defer wg.Done()
    a.lock.Lock()
    a.balance += amount
    a.lock.Unlock()
}

func (a *Account) GetBalance() int {
    a.lock.Lock()
    defer a.lock.Unlock()

    return a.balance
}
Enter fullscreen mode Exit fullscreen mode

and the client code will now look like this:

    var account Account
    var wg sync.WaitGroup

    account.Name = "Test account"

    for i := 0; i < 20; i++ {
        wg.Add(1)
        go account.Deposit(100, &wg)
    }

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go account.Withdraw(100, &wg)
    }

    wg.Wait()
    fmt.Printf("Balance: %d\n", account.GetBalance())
Enter fullscreen mode Exit fullscreen mode

Now, it is more consistent, and the value of the balance will be the same.

Problem solved, why we need RWMutex?

That's fair enough. But the problem is, that when using Mutex the value from the memory will be locked until the Unlock method will be invoked. This is also valable for the reading phase. In order to make reading accessible for multiple threads, the Mutex can be replaced with RWMutex, and for reading it will be used RLock and RUnlock methods.

In this case the code will look something like this:

type Account struct {
    balance int
    Name    string
    lock    sync.RWMutex
}

func (a *Account) Withdraw(amount int, wg *sync.WaitGroup) {
    defer wg.Done()

    a.lock.Lock()
    defer a.lock.Unlock()
    a.balance -= amount

}

func (a *Account) Deposit(amount int, wg *sync.WaitGroup) {
    defer wg.Done()

    a.lock.Lock()
    defer a.lock.Unlock()
    a.balance += amount
}

func (a *Account) GetBalance() int {
    a.lock.RLock()
    defer a.lock.RUnlock()

    return a.balance
}
Enter fullscreen mode Exit fullscreen mode

Note, that other thing that changed, is the defer unlock for Deposit and Withdraw. This may be useful for other complex operations, that may result into unlocked locks, due to failures, that may occur to the code.

Conclusion

Race conditions very often are hard to detect, and can lead to serious bugs in system functionality. The most straightforward approach would be to use Mutex for locking your critical data. The RWMutex can be used if, and only if there are a limited number of concurrent readers of that data. Starting from a number of concurrent readers, it may become ineffective to use RWMutex.

Discussion (0)