Race Condition & Critical Section¶
What is a race condition: A race condition occurs when two or more threads access shared data concurrently, at least one access is a write, and the final result depends on the non-deterministic order of execution. The program produces different results on different runs — sometimes correct, sometimes wrong, and almost impossible to reproduce in debugging.
Why it happens:
counter = counter + 1 looks like one operation but is three machine instructions: LOAD (read counter into register), ADD (increment register), STORE (write register back to memory). If two threads execute these simultaneously, the interleaving can be:
Result: counter is 1 instead of 2. One increment was lost. In a bank balance scenario, this means money disappears.
What problem it solves — critical section: A critical section is the code region that accesses shared data and must not be executed by more than one thread at a time. Protecting the critical section prevents race conditions.
Fixing a race condition:
Mutex: wrap the critical section with a lock. Only one thread holds the lock at a time; others block until it's released.
var mu sync.Mutex
mu.Lock()
counter++
mu.Unlock()
Atomic operation: for simple operations (increment, compare-and-swap), use CPU atomic instructions — no lock needed, single indivisible machine instruction.
atomic.AddInt64(&counter, 1)
Channel (Go): communicate through channels instead of sharing memory. One goroutine owns the counter; others send update requests through a channel.
Trade-offs: More synchronization = safer but slower. A highly contended mutex becomes a serialization point — all threads queue up, eliminating parallelism. Atomic operations are faster than mutexes but only work for simple operations. Lock-free data structures use CAS loops — no blocking, but complex to implement correctly.
Real-world usage: if two users simultaneously buy the last item in stock: both goroutines read stock = 1, both pass the stock > 0 check, both proceed to purchase. Result: stock = -1 — item sold twice. Fix: SELECT ... FOR UPDATE in PostgreSQL (database-level row lock), or a mutex around the read-check-decrement in the application, or optimistic locking (UPDATE stock WHERE stock > 0 AND stock = 1).