What is Race Condition in Golang ? [3 Easy Examples]

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.

 

What is Race Condition in Golang ? [3 Easy Examples]

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()
}
OUTPUT
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

 

In the output, notice that the “Ending Vehicle count” value  after executing multiple time is not constant. Ideally it should be same as “Starting Vehicle count” value  but this is not the case. Hence, this proves that race condition has occurred intermittently. Below diagram will help you understand why this condition occurred intermittently.
In the above diagram, VehicleRecord is a shared resource. If both VehicleSale() and VehiclePurchase() try to manipulate the data at the same time without knowing the operation other one is doing , then they will end up writing wrong data. Let’s now see how to prevent this problem in below examples.

 

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()
}

OUTPUT
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()
}
OUTPUT
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.

 

 

Leave a Comment