Understanding Mutex in Go

Kamna Garg
5 min readJul 7, 2023
Image source: google

Introduction

In concurrent programming, ensuring data integrity and preventing race conditions is crucial. In Go, the sync.Mutex type provides a simple and effective way to achieve mutual exclusion and control concurrent access to shared resources. In this blog post, we will explore the concept of mutexes, understand how to use them in Go, and discuss their internals, and their role in solving race conditions.

What is a Mutex and how it solves race conditions:

A mutex, short for mutual exclusion, is used to protect shared resources from simultaneous access by multiple goroutines. It ensures that only one goroutine can access a critical section of code at a time. Race conditions occur when multiple goroutines access and modify shared data concurrently, leading to unpredictable and erroneous behavior. Mutexes prevent race conditions by allowing only one goroutine to acquire the lock and access the shared resource, while other goroutines wait until the lock is released.

Mutexes are data structures provided by the standard sync package.

Understanding Mutex Internals

Let’s try to understand how mutex prevents race conditions. It’s time to delve into the internals of mutex.

Internally, the sync.Mutex type in Go utilizes low-level atomic operations provided by the underlying processor architecture. These atomic operations ensure that the mutex operations themselves are thread-safe and efficient.

To understand the role of low-level atomic operations, let’s take a closer look at the implementation of the sync.Mutex type. While the exact implementation details may vary depending on the target platform, we can examine a simplified version that highlights the essential elements:

In the simplified implementation above, the Mutex struct includes a state variable, which represents the lock's state, and a sema variable, which is a semaphore used for blocking and waking up goroutines.

The Lock() method attempts to acquire the lock by using the atomic.CompareAndSwapInt32() function. This function atomically compares the state variable's value with 0 and swaps it with 1 if they are equal. If the swap is successful, the lock is acquired without blocking. Otherwise, the Lock() method calls runtime_SemacquireMutex() to wait for the lock to become available.

The Unlock() method releases the lock by using the atomic.CompareAndSwapInt32() function again. It compares the state variable's value with 1 and swaps it with 0 if they are equal. If the swap is successful, indicating that the lock was held, the Unlock() method calls runtime_SemreleaseMutex() to wake up any waiting goroutine.

The use of atomic operations ensures that the lock’s state is updated atomically, without interference from other goroutines. This atomicity guarantees thread safety and eliminates the need for additional locks or synchronization mechanisms.

How to prevent race conditions

In Go, the sync package provides the Mutex type, which includes two main methods: Lock() and Unlock().

To understand how a mutex solves race conditions, let’s consider an example without using a mutex:

Below is the output :

Output

Here multiple goroutines are simultaneously reading and updating the value of the counter without any proper synchronization.

Race condition

We need to use the sync.Mutex type to prevent multiple goroutines from accessing counter at the same time:

Below is the output:

Output
No race conditions

Where Not to Use Mutex

  1. High Contention: If many goroutines are frequently trying for the same lock, the performance of mutexes can degrade. In such cases, consider using alternative synchronization primitives like sync.RWMutex or channel-based communication patterns.
  2. Deadlock Risks: Improper use of mutexes can lead to deadlocks, where goroutines end up waiting indefinitely for a lock to be released. Avoid complex nesting of locks or forgetting to unlock the mutex.

Using defer with Unlock()

It is very easy to miss unlocking the mutex.

Whenever you call the Lock method, you must ensure that Unlock is eventually called, otherwise any goroutine trying to acquire the same lock will be blocked forever.

In the above example, if either goroutine1 or goroutine2 acquires the lock, the unlocked goroutine will wait for the lock indefinitely. Here the if condition is always true and it will never unlock the mutex.

output

We can use defer here to prevent such kind of scenarios. Without defer, forgetting to manually release the lock before returning from a function can lead to deadlocks, where a goroutine may be blocked indefinitely.

output

The defer statement in Go allows us to postpone the execution of a function until the surrounding function returns. By deferring the Unlock() method immediately after acquiring the lock, we ensure that the mutex will always be released, even if an error occurs or a panic is triggered.

Conclusion

Mutexes play a crucial role in concurrent programming to ensure proper synchronization and prevent race conditions. By using the sync.Mutex type in Go, developers can protect shared resources and control access to critical sections of code.

If you would like to view the full codebase, please visit the repository by clicking here: Repo

--

--

Kamna Garg

Software Developer, Women in tech, Seeker, Love writing, Always a student, IIT Kanpur