Introduction
A race condition occurs when program correctness depends on timing between concurrent executions. The same code may give different results because operations from different threads, processes, requests, or workers happen in a different order.
This usually happens around shared state. If multiple executions can access the same data or resource, and at least one of them changes it, an unlucky interleaving can break the expected result.
What Makes a Race Condition Unsafe
Not all concurrency is unsafe. Concurrency becomes risky when executions interact with the same state and the order of their actions is not controlled.
A race condition can happen around many kinds of shared state:
Memory: Two threads update the same variable or object.
Queue: Multiple workers try to claim the same job.
File: Two processes check and create the same file.
Cache entry: One execution refreshes data while another reads old or partial state.
Database row: Two requests make decisions using the same stored record.
The important point is not only that something is shared. The problem begins when correctness depends on who reads, writes, or acts first.
What Makes a Race Condition Unsafe
Three Ingredients of a Race
A race risk usually appears when three ingredients come together.
Shared state: More than one execution can access the same data or resource.
Mutation: At least one execution writes, updates, creates, deletes, or changes that state.
Uncontrolled interleaving: Steps from different executions can mix in different orders.
If changing the timing changes the final result, the design is not stable. Correctness should come from controlled access, not from lucky scheduling.
Race Condition vs Data Race
Race condition and data race are related, but they are not identical.
Race condition: A broad correctness problem where the result depends on timing or ordering between concurrent executions
Data race: A narrower memory-specific case where the same memory location is accessed concurrently, at least one access is a write, and proper synchronization is missing
A data race is usually about memory access. A race condition can also involve files, queues, cache entries, database rows, external resources, or multi-step decisions.
Simple Counter Example
Suppose a shared variable counter starts at 0. Thread A and Thread B both run counter++. The expected result is 2, but the final value may become 1.
The reason is that counter++ may behave like three smaller steps:
load counter
add 1
store counterA bad interleaving can look like this:
Step | Thread A | Thread B | Counter |
|---|---|---|---|
1 | Reads 0 | - | 0 |
2 | Pauses | Reads 0 | 0 |
3 | Adds 1 | Adds 1 | 0 |
4 | Stores 1 | - | 1 |
5 | - | Stores 1 | 1 |
Both threads performed an increment, but both were based on the old value 0. One update was lost because the read-modify-write operation was not controlled.
Race Condition
Check-Then-Act Example
Race conditions also happen outside simple counters. Consider two requests trying to create the same username.
Each request does two steps:
Check: See whether the username is available.
Act: Create the username if it is available.
If both requests check before either creation becomes visible, both may decide the username is free. The individual logic looks reasonable, but the combined timing is unsafe.
This is called a check-then-act race. The problem is that the world can change between the check and the action.
How to Identify a Race Condition
To identify a race, look for the unsafe decision, not just the shared object.
Useful questions are:
What is shared? Find the memory, file, queue, cache entry, database row, or resource.
Who reads and writes it? Identify which executions observe and modify the state.
Can the steps overlap? Check whether two executions can act during the same time window.
Is correctness multi-step? Look for logic that must keep a check and action together.
Does timing change the result? If a different interleaving changes correctness, it is a race risk.
Two common risky patterns are read-modify-write and check-then-act. Both look simple in one execution but can break when concurrent executions overlap.
Handling Race Conditions
A race condition can be handled by removing or controlling one of its ingredients.
Remove shared state: Give each execution its own state when possible.
Use immutable data: If data is not changed, competing writes disappear.
Transfer ownership: Use queues or handoff designs so only one execution owns the state at a time.
Control access: Use synchronization when ordering matters.
Make operations atomic: Ensure related steps behave like one indivisible operation.
Synchronization tools such as locks, semaphores, monitors, critical sections, atomic operations, and database constraints are useful, but the first step is understanding what must be protected.
Summary
A race condition is a timing-dependent correctness failure. It appears when concurrent executions interact with shared state, at least one execution changes that state, and the final result depends on uncontrolled ordering.
Race conditions can involve memory, queues, files, caches, database rows, requests, workers, threads, or processes. The clean way to reason about them is to find the shared state, find the mutation, and control the interleaving when correctness depends on order.
Be the first to add a comment.