15 minlesson

Race Conditions

Race Conditions

A race condition occurs when multiple threads access shared data concurrently, and the outcome depends on the timing of their execution. Understanding race conditions is essential for writing correct concurrent code.

What Is a Race Condition?

csharp
1int counter = 0;
2
3// Two threads incrementing the same counter
4void Increment()
5{
6 for (int i = 0; i < 100000; i++)
7 {
8 counter++; // NOT thread-safe!
9 }
10}
11
12Thread t1 = new Thread(Increment);
13Thread t2 = new Thread(Increment);
14t1.Start(); t2.Start();
15t1.Join(); t2.Join();
16
17Console.WriteLine(counter); // Expected: 200000, Actual: varies (less)

Why Does This Happen?

The counter++ operation is NOT atomic. It actually involves three steps:

11. READ: Load counter value from memory
22. MODIFY: Add 1 to the value
33. WRITE: Store result back to memory

When threads interleave these steps:

1Thread 1 Thread 2
2-------- --------
3READ counter (= 5)
4 READ counter (= 5)
5ADD 1 (= 6)
6 ADD 1 (= 6)
7WRITE counter (= 6)
8 WRITE counter (= 6)
9
10Result: counter = 6, but we expected 7!

Types of Race Conditions

Check-Then-Act

csharp
1// RACE: Another thread could modify between check and act
2if (!dictionary.ContainsKey(key))
3{
4 dictionary[key] = value; // Another thread might have added it!
5}

Read-Modify-Write

csharp
1// RACE: Value could change between read and write
2balance = balance + deposit; // Not atomic

Compound Operations

csharp
1// RACE: List could change between Count check and Add
2if (list.Count < maxSize)
3{
4 list.Add(item); // Another thread might have filled it!
5}

Demonstrating a Race Condition

csharp
1class BankAccount
2{
3 public decimal Balance { get; private set; } = 1000;
4
5 public void Withdraw(decimal amount)
6 {
7 if (Balance >= amount) // Check
8 {
9 Thread.Sleep(1); // Simulate processing (makes race more likely)
10 Balance -= amount; // Act
11 }
12 }
13}
14
15// Two threads trying to withdraw the full balance
16var account = new BankAccount();
17var t1 = new Thread(() => account.Withdraw(1000));
18var t2 = new Thread(() => account.Withdraw(1000));
19
20t1.Start(); t2.Start();
21t1.Join(); t2.Join();
22
23Console.WriteLine(account.Balance); // Could be -1000! Both threads passed the check.

Symptoms of Race Conditions

Race conditions are notorious for being:

  1. Intermittent - May only occur under specific timing
  2. Hard to reproduce - May work 99% of the time
  3. Heisenbugs - May disappear when you add debugging code
  4. Environment-dependent - May only occur on production servers

Common Race Condition Scenarios

Lazy Initialization

csharp
1// RACE: Multiple threads could create instances
2private static ExpensiveObject? _instance;
3
4public static ExpensiveObject Instance
5{
6 get
7 {
8 if (_instance == null) // Both threads see null
9 {
10 _instance = new ExpensiveObject(); // Both create!
11 }
12 return _instance;
13 }
14}

Caching

csharp
1// RACE: Cache might be populated multiple times
2if (!cache.ContainsKey(key))
3{
4 var value = ExpensiveComputation(key);
5 cache[key] = value; // Another thread might have already cached it
6}

Event Handlers

csharp
1// RACE: Handler could become null between check and invoke
2if (OnCompleted != null)
3{
4 OnCompleted(this, EventArgs.Empty); // OnCompleted could be null now!
5}
6
7// Safe pattern:
8var handler = OnCompleted;
9handler?.Invoke(this, EventArgs.Empty);

Detecting Race Conditions

Thread Sanitizers

Use tools like:

  • Visual Studio's Concurrency Visualizer
  • dotnet-trace
  • Race condition analyzers

Stress Testing

csharp
1// Run many threads to increase chance of race
2var tasks = Enumerable.Range(0, 100)
3 .Select(_ => Task.Run(() => SuspiciousOperation()))
4 .ToArray();
5
6await Task.WhenAll(tasks);

Assertions

csharp
1// Add assertions that fail if race occurs
2Debug.Assert(counter >= 0, "Race condition detected!");

Solutions Preview

Race conditions are solved through synchronization:

TechniqueUse Case
lockGeneral mutual exclusion
SemaphoreSlimLimiting concurrent access
InterlockedAtomic operations on primitives
Concurrent collectionsThread-safe data structures
Immutable dataNo shared mutable state

These are covered in the following lessons.

Key Takeaways

  • Race conditions occur when threads access shared data concurrently
  • They cause unpredictable, hard-to-reproduce bugs
  • Common patterns: check-then-act, read-modify-write
  • The fix is synchronization (locks, semaphores, etc.)
  • Concurrent collections provide thread-safe alternatives
  • Best practice: minimize shared mutable state
Race Conditions - Anko Academy