Understanding Race Condition
In this tutorial, we will learn about what is race condition in Golang using 3 easy examples. Let us first understand race condition in lay man term. Imagine you and your friend are in playground and want to slide down the playground slide. The only rule here is that you both can not be on the slide at the same time so you need to take turns. If you both try to slide down at the same time, there will be confusion and event accidents. In this scenario:
- The shared resource is playground slide
- You and your friend are like different parts of a program trying to access the resource.
In Operating System, this concept remains the same. When multiple parts of a program try to modify shared data simultaneously without proper synchronization , a race condition can occur, leading to unpredictable behavior or even crashes. To prevent race conditions, synchronization mechanisms like locks and mutexes are used which ensures that the shared data is accessed in a controlled manner. In this tutorial, we will look at an example where race condition is occurred and then we will look at other examples where we will prevent the race condition using mutex and locks.
Understanding Mutex
Also read: CPU Bound Processes in Golang [4 Best Examples]
Mutex stands for Mutually Exclusive. Let’s understand Mutex in lay man term, Suppose you visit a doctor in a hospital and there are lot many other patients waiting for the consultancy. Doctor will call one patient at a time, lock the door, do the checkup and once done release the lock. Then he will call another patient do the same thing and once done unlock the door. During the checkup, if anyone else try to get in, he can not as the door is locked and he has to wait until key is released( i.e door is open and doctor is available).
Mutex does the similar operation in programming world. It locks the resource getting used by one goroutine. It releases the lock and make it available to other goroutines only after execution is done and lock is releases. Below is the sequence in which mutex is implemented in a program.
mutex = sync.Mutex{} – declare mutex
mutex.Lock() – lock the resource
piece of code – execute code between the locks
mutex.Unlock – unlock the resource
What is Race Condition in Golang ? [3 Easy Examples]
Also read: What is I/O Bound Process [3 Best Examples] ?
Race condition in Golang is same in any other programming languages. It it a condition where two or more goroutines access shared data concurrently and at least one of them modifies the data. This can lead to unexpected and incorrect behavior because the order of execution between the goroutines is not guaranteed and they might interrupt each other’s execution.
We will look at examples in the next section which will implement the scenario of Vehicle Purchase and Sale. Let us understand the scenario first which we will implement in this tutorial before jumping into the code.
We have initiated VehicleRecord to 1000 units. In the func main(), we first prints the intial VehicleRecord count. We then setup a waitgroup where two goroutines are added. We then call two goroutines, VehicleSales(), VehiclePurchases(). We will wait until they finishes their execution then we will print the vehicle count again. These goroutines will do the following operation:
VehicleSales() – set up a for loop to execute 3000 times. After each iteration of for loop, VehicleRecord is decrement by 100 units which means 100 Vehicles are sold.
VehiclePurchases() – set up a for loop to execute 3000 times. After each iteration of the for loop, VehicleRecord is increment by 100 units which means 100 Vehicles are purchased.
So what we are doing here is, we are subtracting 100 from VehicleRecord via VehicleSales() function and adding 100 to VehicleRecord via VehiclePurchases() function. so at any point of time, the value of VehicleRecord should remains 1000, that is the expectation. Now, let’s implement this scenario in below examples.
Example-1: Create Race Condition in Golang
In this example, we will implement the above scenario in Golang and see how race condition occurred intermittently.
package main import ( "fmt" "sync" ) var ( wg sync.WaitGroup VehicleRecord int32 = 1000 ) func main() { fmt.Println("Starting Vehicle count = ", VehicleRecord) wg.Add(2) go VehicleSales() go VehiclePurchases() wg.Wait() fmt.Println("Ending Vehicle count = ", VehicleRecord) } func VehicleSales() { for i := 0; i < 3000; i++ { VehicleRecord -= 100 } wg.Done() } func VehiclePurchases() { for i := 0; i < 3000; i++ { VehicleRecord += 100 } wg.Done() }
PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000 PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 19400 PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = -172700 PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000
Example-2: Prevent Race Condition Using Mutex
The most common solution to prevent the race condition is to use the Mutex. We have declared a mutex. this mutex is used by both the goroutine while executing the for loop which will update the shared resource i.e VehicleRecord.
package main import ( "fmt" "sync" ) var ( wg sync.WaitGroup mutex = sync.Mutex{} VehicleRecord int32 = 1000 ) func main() { fmt.Println("Starting Vehicle count = ", VehicleRecord) wg.Add(2) go VehicleSales() go VehiclePurchases() wg.Wait() fmt.Println("Ending Vehicle count = ", VehicleRecord) } func VehicleSales() { for i := 0; i < 3000; i++ { mutex.Lock() VehicleRecord -= 100 mutex.Unlock() } wg.Done() } func VehiclePurchases() { for i := 0; i < 3000; i++ { mutex.Lock() VehicleRecord += 100 mutex.Unlock() } wg.Done() }
PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000 PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000 PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000
Example-3: Prevent Race Condition Using Atomic Variable
In this example, we will prevent the race condition using atomic variable. Atomic variable is just a nice way of implementing the same code what we have implemented using mutex.
package main import ( "fmt" "sync" "sync/atomic" ) var ( wg sync.WaitGroup mutex = sync.Mutex{} VehicleRecord int32 = 1000 ) func main() { fmt.Println("Starting Vehicle count = ", VehicleRecord) wg.Add(2) go VehicleSales() go VehiclePurchases() wg.Wait() fmt.Println("Ending Vehicle count = ", VehicleRecord) } func VehicleSales() { for i := 0; i < 3000; i++ { atomic.AddInt32(&VehicleRecord, -100) } wg.Done() } func VehiclePurchases() { for i := 0; i < 3000; i++ { atomic.AddInt32(&VehicleRecord, +100) } wg.Done() }
PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000 PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000 PS C:\Users\linuxnasa\OneDrive\Desktop\Go-Dump> go run .\race-condition.go Starting Vehicle count = 1000 Ending Vehicle count = 1000
Summary
We learnt about race condition and different ways to prevent the race condition in Golang. There is something called data race condition also which occurs when two threads access the same mutable object without synchronization. Data race condition is independent of race condition. You can read more about data read condition here.