Race Condition

Learn about the race condition.

What is a race condition?

A race condition occurs when two or more threads have access to the same data and both try to update it simultaneously. We don’t know the order in which the threads will attempt to access the shared data because the scheduling algorithm can switch between threads. As a result, the outcome is dependent on the scheduling method of the thread, which means that both threads race to access and alter the data.

Race conditions are a very common problem in concurrent and parallel programming. When poorly planned, concurrent programming can cause huge problems since it utilizes the CPU core to the maximum extent.

function update(){
    x = 100
    thread_decrement(){
        x = x - 50
    }
    thread_increment(){
        x = x + 15
    }
}

The pseudocode above has two threads. In one thread, an increment function runs, while in the other, a decrement function runs. The scheduler is preemptive in nature. It could preempt the process if it’s waiting for some other resources in order to use the CPU core concurrently.

In the code section above, three scenarios can take place. Variable x is the shared resource between the two processes. The three scenarios are as follows:

  • Everything works in order. The update function starts executing, then the decrement function starts running and changes the value of the x variable to 50. Then the scheduler preempts the decrement function, and the increment function executes and changes the value to 65.
  • The value of variable x is 100. The decrement function starts its execution and reads the value of variable x. But before it can update its value, preemption takes place. The increment function starts its execution and changes the value to 115. Again, it will go to the decrement function. The value of the variable stored with the decrement function is 100. It will update the value of variable x from 115 to 50. The condition is a race condition because the two functions are racing to update the value.
  • The value of variable x is 100. The decrement function starts its execution and reads the value of variable x. But before it can update its value, preemption takes place. The increment function starts its execution. Again, the value read by the increment function is 100, but the preemption happens before it can update the value of x. The scheduler starts the execution of the decrement function. It will update the value of variable x from 100 to 50. Again, it will move to the increment function. The value of x stored with the increment function is 100, and it will change the value from 50 to 115.

Race condition in Golang

Let’s look at a practical example to understand race conditions better.

package main
import (
"time"
)
// This is an example of race condition
// 2 goroutines try to write and there is no access control.
var x int = 0
func decrement() {
for {
x = x - 50
}
}
func increment() {
for {
x = x + 15
}
}
func main() {
go increment()
go decrement()
time.Sleep(5 * time.Second)
}

The code above contains a race condition (lines 14 and 20). The two threads are racing to update the value which causes one to overlap with the other. It may run in the correct order, but there’s no guarantee. Since we want the Go compiler to detect the race condition, we’ll run it with the race flag (go run -race main.go). It will display the following error:

$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x0000005464b8 by goroutine 6:
  main.increment()
      /usercode/main.go:20 +0x29

Previous write at 0x0000005464b8 by goroutine 7:
  main.decrement()
      /usercode/main.go:14 +0x44

Goroutine 6 (running) created at:
  main.main()
      /usercode/main.go:25 +0x29

Goroutine 7 (running) created at:
  main.main()
      /usercode/main.go:26 +0x35
==================
Found 1 data race(s)
exit status 66

Explanation

The important lines in the code:

  • Line 14: We try to decrease the value of the shared variable. The decrement function accesses the x variable without synchronization.

  • Line 20: We try to increase the value of the shared variable. The increment function accesses the x variable without synchronization.

  • Line 27: We add the time.Sleep function to stop the main goroutine from exiting the program.