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:

sequenceDiagram participant T1 as Thread 1 participant Mem as Memory (counter) participant T2 as Thread 2 T1->>Mem: LOAD (reads 0) T2->>Mem: LOAD (reads 0) T1->>T1: ADD -> 1 T2->>T2: ADD -> 1 T1->>Mem: STORE 1 T2->>Mem: STORE 1 Note over Mem: final value is 1, not 2 — T1's increment lost

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).