Aikido

How to prevent race conditions: thread-safe access to shared state

Bug Risk

Rule
Ensure thread-safe access to shared state.
Shared mutable state accessed by multiple threads
without synchronization causes race conditions and runtime errors.

Supported languages: Python, Java, C#

Introduction

When multiple threads access and modify shared variables without synchronization, race conditions occur. The final value depends on unpredictable thread execution timing, leading to data corruption, incorrect calculations, or runtime errors. A counter incremented by multiple threads without locking will miss updates as threads read stale values, increment them, and write back conflicting results.

Why it matters

Data corruption and incorrect results: Race conditions cause silent data corruption where values become inconsistent or incorrect. Account balances can be wrong, inventory counts can be negative, or aggregated statistics can be corrupted. These bugs are difficult to reproduce because they depend on exact thread timing.

System instability: Unsynchronized access to shared state can crash applications. One thread might modify a data structure while another reads it, causing exceptions like null pointer errors or index out of bounds. In production, these manifest as intermittent crashes under load.

Debugging complexity: Race conditions are notoriously difficult to debug because they're non-deterministic. The bug might not appear in single-threaded tests or low-load environments. Reproduction requires specific thread interleaving that's hard to force, making issues appear and disappear randomly.

Code examples

❌ Non-compliant:

class BankAccount:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        current = self.balance
        # Race condition: another thread can modify balance here
        time.sleep(0.001)  # Simulates processing time
        self.balance = current + amount

    def withdraw(self, amount):
        if self.balance >= amount:
            current = self.balance
            time.sleep(0.001)
            self.balance = current - amount
            return True
        return False

Why it's wrong: Multiple threads calling deposit() or withdraw() simultaneously create race conditions. Two threads depositing $100 each might both read balance as $0, then both write $100, resulting in final balance of $100 instead of $200.

✅ Compliant:

import threading

class BankAccount:
    def __init__(self):
        self.__balance = 0
        self.__lock = threading.Lock()

    @property
    def balance(self):
        with self.__lock:
            return self.__balance

    def deposit(self, amount):
        with self.__lock:
            current = self.__balance
            time.sleep(0.001)
            self.__balance = current + amount

    def withdraw(self, amount):
        with self.__lock:
            if self.__balance >= amount:
                current = self.__balance
                time.sleep(0.001)
                self.__balance = current - amount
                return True
            return False

Why this matters: The threading.Lock() ensures only one thread accesses balance at a time. When one thread holds the lock, others wait, preventing simultaneous modifications. Private __balance with readonly @property prevents external code from bypassing the lock protection.

Conclusion

Protect all shared mutable state with appropriate synchronization primitives like locks, semaphores, or atomic operations. Prefer immutable data structures or thread-local storage when possible. When synchronization is necessary, minimize critical sections to reduce contention and improve performance.

FAQs

Got Questions?

What synchronization primitives should I use?

Use locks (mutex) for exclusive access to shared state. Use semaphores for limiting concurrent access to resources. Use condition variables for thread coordination and signaling. For simple counters or flags, atomic operations are faster than locks. Choose based on your concurrency pattern: locks for mutual exclusion, atomics for simple operations, higher-level constructs like queues for producer-consumer patterns.

How do I avoid deadlocks when using multiple locks?

Always acquire locks in the same order across all code paths. If function A needs locks X and Y, and function B needs locks Y and X, acquire them in consistent order (always X then Y). Use timeout-based lock acquisition to detect potential deadlocks. Better yet, redesign to need only one lock per critical section, or use lock-free data structures.

What's the performance impact of synchronization?

Lock contention slows down highly concurrent code because threads wait for lock holders to release. However, incorrect unsynchronized code is infinitely slower because it produces wrong results. Minimize lock scope (critical sections) to only protect state modification. Use read-write locks when multiple readers don't conflict. Profile before optimizing, correctness comes first.

Can I use thread-local storage instead of locks?

Yes, when each thread needs its own copy of data. Thread-local storage eliminates synchronization overhead by giving each thread private state. Use for per-thread caches, buffers, or accumulators that get merged later. However, you still need synchronization when threads communicate or share final results.

What about Python's Global Interpreter Lock (GIL)?

The GIL doesn't eliminate the need for locks. While it prevents simultaneous Python bytecode execution, it doesn't make operations atomic. A simple increment counter += 1 involves multiple bytecode operations where the GIL can be released between them. Always use proper synchronization for shared state, even in CPython.

How do I test for race conditions?

Use thread sanitizers and concurrency testing tools specific to your language. Write stress tests that spawn many threads performing concurrent operations and assert invariants hold. Increase thread count and iterations to expose timing-dependent bugs. However, passing tests don't prove absence of race conditions, so code review and careful synchronization design remain critical.

What are lock-free and wait-free data structures?

Lock-free data structures use atomic operations (compare-and-swap) instead of locks, guaranteeing system-wide progress even if threads are delayed. Wait-free structures guarantee per-thread progress. These are complex to implement correctly but offer better performance under high contention. Use battle-tested libraries (java.util.concurrent, C++ atomic library) rather than implementing your own.

Get secure for free

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.