Race Conditions
A race condition occurs when two or more goroutines access shared data concurrently, and at least one of the accesses is a write. This can lead to unpredictable behavior and hard-to-debug issues.
Understanding Race Conditions#
Race conditions happen when the outcome of a program depends on the timing or order of execution of multiple goroutines. If the accesses to shared data aren't properly synchronized, the program may behave incorrectly or produce different results on different runs.
A Simple Example of a Race Condition#
Here's a simple program that demonstrates a race condition:
package main
import (
string">"fmt"
string">"sync"
)
var msg string
var wg sync.WaitGroup
func updateMessage(s string) {
defer wg.Done()
msg = s
}
func main() {
msg = string">"Hello, world!"
wg.Add(2)
go updateMessage(string">"Hello, one")
go updateMessage(string">"Hello, two")
wg.Wait()
fmt.Println(msg)
}
In this program:
- We have a global variable
msg
- We launch two goroutines that both modify
msg
- The final value of
msg
depends on which goroutine executes last - The output could be either "Hello, one" or "Hello, two"
Detecting Race Conditions#
Go includes a built-in race detector that can help identify race conditions in your code. To use it, run your program with the -race
flag:
go run -race .
If we run our example with the race detector:
go run -race .
==================
WARNING: DATA RACE
Write at 0x0000011d09b0 by goroutine 8:
main.updateMessage()
/path/to/file.go:13 +0x78
main.main·dwrap·3()
/path/to/file.go:22 +0x47
Previous write at 0x0000011d09b0 by goroutine 7:
main.updateMessage()
/path/to/file.go:13 +0x78
main.main·dwrap·2()
/path/to/file.go:21 +0x47
Goroutine 8 (running) created at:
main.main()
/path/to/file.go:22 +0x164
Goroutine 7 (finished) created at:
main.main()
/path/to/file.go:21 +0xeb
==================
Hello, two
Found 1 data race(s)
exit status 66
The race detector tells us:
- There's a race condition on a memory address
- Two goroutines are writing to the same location
- The specific lines of code involved in the race
The Risks of Race Conditions#
Race conditions can lead to several problems:
- Inconsistent results: Your program produces different results on each run
- Data corruption: Data structures may be left in an inconsistent state
- Memory corruption: In low-level languages, this can cause crashes
- Subtle bugs: The program works most of the time but fails occasionally
Testing for Race Conditions#
You can also use the race detector when running tests:
go test -race ./...
Here's a test that would detect our race condition:
package main
import (
string">"testing"
)
func Test_updateMessage(t *testing.T) {
msg = string">"Hello, world!"
wg.Add(2)
go updateMessage(string">"Hello, one")
go updateMessage(string">"Hello, two")
wg.Wait()
string">"comment">// The test doesn't actually fail, but the race detector
string">"comment">// will report the race condition
if msg != string">"Hello, one" && msg != string">"Hello, two" {
t.Error(string">"Expected message to be updated")
}
}
Real-World Example#
Let's look at a more realistic example involving a shared counter:
package main
import (
string">"fmt"
string">"sync"
)
func main() {
string">"comment">// Variable for bank balance
var bankBalance int
string">"comment">// Print starting balance
fmt.Printf(string">"Initial account balance: $%d.00\n", bankBalance)
string">"comment">// Define weekly revenue
incomes := []int{500, 10, 50, 100}
var wg sync.WaitGroup
wg.Add(len(incomes))
string">"comment">// Loop through 52 weeks and add income
for i, income := range incomes {
go func(i int, income int) {
defer wg.Done()
for week := 1; week <= 52; week++ {
string">"comment">// This is a race condition - multiple goroutines
string">"comment">// are reading and writing bankBalance without synchronization
temp := bankBalance
temp += income
bankBalance = temp
fmt.Printf(string">"On week %d, you earned $%d.00 from source %d\n",
week, income, i)
}
}(i, income)
}
wg.Wait()
string">"comment">// Print final balance
fmt.Printf(string">"Final bank balance: $%d.00\n", bankBalance)
}
In this example, we have multiple goroutines updating a shared bankBalance
variable. Because these updates aren't synchronized, the final balance might be incorrect.
Summary#
- Race conditions occur when multiple goroutines access shared data concurrently
- They lead to unpredictable behavior and inconsistent results
- Go's race detector can help identify race conditions
- Race conditions are a common source of bugs in concurrent programs
- Always be cautious when sharing data between goroutines
In the next lab, we'll learn how to use mutexes to protect shared data and prevent race conditions.