Page Content

Tutorials

Race Conditions In Go And How To Prevent Race Conditions

Race Conditions in Go

In concurrent programming, a race situation, more precisely a data race condition, is when two or more components running concurrently (like goroutines in Go) try to take over or alter a program’s shared resource or variable. A data race occurs, more specifically, when two or more instructions reach the same memory address and at least one of them writes something.

It is generally advised to presume that unless otherwise specified, a certain type of value in Go cannot be utilised concurrently. The goroutines are “racing to use the value,” which is why this scenario is known as a “race condition.”

Causes and Consequences

Usually, race problems occur when several goroutines attempt to change the same shared variable at the same time. The counter++ operation is one typical example. Despite its seeming simplicity, this line of code is really composed of multiple assembly statements: reading the current value, incrementing it, and finally writing the new value back. Numbers may be duplicated or absent if several goroutines run counter++ at the same time because certain increments may be missed. As a result, the order of operations is not guaranteed, leading to unpredictable and inaccurate program states.

Beyond just producing inaccurate results, these problems may lead to system crashes or the access of random data. Programming errors that frequently result in panics Go’s mechanism for exceptions, which is comparable to those of other languages include utilizing mutexes incorrectly and operating on closed channels or nil pointers.

Example

An illustration of a race situation is provided here:

package main
import (
	"fmt"
	"sync"
)
var (
	counter int
	mu      sync.Mutex // The mutex to protect the shared 'counter'
	wg      sync.WaitGroup // A wait group to wait for all goroutines to finish
)
func incr() {
	defer wg.Done() // Decrement the wait group counter when the function returns
	mu.Lock()       // Lock the mutex before accessing the shared variable
	counter++       // Critical section: this operation is now safe
	fmt.Println(counter)
	mu.Unlock()     // Unlock the mutex after the operation is complete
}
func main() {
	wg.Add(20) // Add 20 to the wait group counter
	for i := 0; i < 20; i++ {
		go incr()
	}
	wg.Wait() // Wait for all goroutines to finish
	fmt.Println("Final counter value:", counter)
}

Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Final counter value: 20

Explanation: This code has several incr() goroutines that try to change the global counter variable at the same time. Since counter++ is a multi-step operation (read, increment, write), updates may be missed or overwritten according to the exact timing of goroutine execution. This race will frequently result in irregular output rather than just 1, 2,…, 20.

Preventing Race Conditions

Go offers a number of ways to avoid race situations, favouring the idea of “sharing memory by communicating” via channels as opposed to “communicating by sharing memory” with explicit locks.

Mutexes (sync.Mutex):

  • To protect shared resources against non-atomic activities, a mutex (short for mutual exclusion) is a synchronization primitive that locks a block of code to a single thread (goroutine) at a time.
  • There are Lock() and Unlock() methods in the sync.Mutex class. If the mutex is already locked by another goroutine, the caller goroutine blocks until the lock is released. Otherwise, Lock() gets the lock. Unlock() makes the lock unlocked.
  • Multiple goroutines cannot execute the essential portion of code, which is the segment of code between m.Lock() and m.Unlock().
  • To guarantee that the lock is released when the function finishes, even in cases where there are several return pathways or a panic occurs, the defer statement is commonly used with Unlock().

Read-Write Mutexes (sync.RWMutex):

  • A sync.RWMutex provides better performance than a sync in situations where there are a lot of readers but not many writers.mutex.
  • Only one goroutine can write at a time, while several goroutines can read a shared resource simultaneously.
  • The Lock() and Unlock() methods provide for exclusive write access, while the RLock() and RUnlock() methods allow for shared read access.

Channels and Monitor Goroutines:

  • Channels are Go’s preferred method of concurrency because they allow goroutines to safely transmit and receive values, allowing them to communicate with each other and synchronise their execution.
  • It is possible to employ a monitor goroutine design for more intricate shared states. According to this pattern, the shared data is owned by a single, specialised goroutine, and other goroutines only communicate with it over channels to read or write data. Because the monitor goroutine’s channel operations serialise access to the shared data, this technique ensures that only one goroutine directly alters the shared state, preventing data corruption.

You can also read What Is Mean By Shared Memory In Go With Code Examples

Agarapu Geetha
Agarapu Geetha
My name is Agarapu Geetha, a B.Com graduate with a strong passion for technology and innovation. I work as a content writer at Govindhtech, where I dedicate myself to exploring and publishing the latest updates in the world of tech.
Index