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 mainimport (string">"fmt"string">"sync")var msg stringvar wg sync.WaitGroupfunc 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
msgdepends 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 RACEWrite at 0x0000011d09b0 by goroutine 8:main.updateMessage()/path/to/file.go:13 +0x78main.main·dwrap·3()/path/to/file.go:22 +0x47Previous write at 0x0000011d09b0 by goroutine 7:main.updateMessage()/path/to/file.go:13 +0x78main.main·dwrap·2()/path/to/file.go:21 +0x47Goroutine 8 (running) created at:main.main()/path/to/file.go:22 +0x164Goroutine 7 (finished) created at:main.main()/path/to/file.go:21 +0xeb==================Hello, twoFound 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 mainimport (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 detectorstring">"comment">// will report the race conditionif 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 mainimport (string">"fmt"string">"sync")func main() {string">"comment">// Variable for bank balancevar bankBalance intstring">"comment">// Print starting balancefmt.Printf(string">"Initial account balance: $%d.00\n", bankBalance)string">"comment">// Define weekly revenueincomes := []int{500, 10, 50, 100}var wg sync.WaitGroupwg.Add(len(incomes))string">"comment">// Loop through 52 weeks and add incomefor 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 goroutinesstring">"comment">// are reading and writing bankBalance without synchronizationtemp := bankBalancetemp += incomebankBalance = tempfmt.Printf(string">"On week %d, you earned $%d.00 from source %d\n",week, income, i)}}(i, income)}wg.Wait()string">"comment">// Print final balancefmt.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.