llmstory
Race Condition in Multi-threaded Counter with Mutex Fix

Understanding Race Conditions and Global Counters

A race condition in concurrent programming occurs when multiple threads or processes access and manipulate shared data simultaneously, and the final outcome depends on the specific order in which these operations are executed. This non-deterministic behavior can lead to unpredictable and often incorrect results.

Consider a common scenario in a multi-threaded web service: a global counter that tracks the number of incoming requests. Each time a request arrives, a thread increments this shared counter. Intuitively, if 100 requests arrive, the counter should reach 100. However, due to race conditions, the final count might be significantly lower.

The core of the problem lies in the 'read-modify-write' cycle inherent in a simple increment operation. An operation like counter = counter + 1 or counter++ is not a single, atomic step. Instead, it typically breaks down into three distinct CPU instructions:

  1. Read: The thread reads the current value of the GLOBAL_COUNTER from memory into a CPU register.
  2. Modify: The thread increments the value in its local CPU register.
  3. Write: The thread writes the new, incremented value from its CPU register back to the GLOBAL_COUNTER in shared memory.

In a multi-threaded environment, these steps can interleave, leading to lost updates. Imagine the following sequence of events involving two threads, Thread A and Thread B, trying to increment a counter currently at 100:

  • Thread A: Reads GLOBAL_COUNTER (value is 100).
  • Thread B: Reads GLOBAL_COUNTER (value is 100).
  • Thread A: Modifies its local copy to 101.
  • Thread B: Modifies its local copy to 101.
  • Thread A: Writes 101 back to GLOBAL_COUNTER in memory.
  • Thread B: Writes 101 back to GLOBAL_COUNTER in memory.

In this scenario, despite two increments being attempted, the GLOBAL_COUNTER ends up at 101 instead of the correct 102. One increment was effectively 'lost' because Thread B overwrote Thread A's update. This demonstrates why concurrent, unsynchronized access to shared resources like a global counter can lead to a lower-than-actual count in a web service.

1.

In your own words, explain why a global counter incremented by multiple threads in a web service environment can lead to a lower-than-actual count. Reference the 'read-modify-write' problem in your explanation.

Pseudo-code Demonstration of the Problem (Unsafe Counter)

This pseudo-code illustrates a common scenario where a global counter is incremented by multiple threads without any synchronization, making it vulnerable to race conditions.

// Global shared variable, accessible by all threads
GLOBAL_COUNTER = 0

// Function executed by multiple threads (e.g., for each web service request)
FUNCTION increment_counter():
    // CRITICAL SECTION START: This block is where concurrent access can cause issues
    // Step 1: Read the current value of the global counter
    current_value = GLOBAL_COUNTER

    // Step 2: Modify (increment) the value locally
    current_value = current_value + 1

    // Step 3: Write the new value back to the global counter
    GLOBAL_COUNTER = current_value
    // CRITICAL SECTION END

    PRINT "Counter incremented to:", GLOBAL_COUNTER

// Simulate a web service request being handled by a thread
FUNCTION handle_request():
    increment_counter()

// Conceptual Main program flow:
// CREATE N_THREADS (e.g., 5 threads representing concurrent requests)
// FOR EACH thread IN N_THREADS:
//     START thread, executing handle_request()
// JOIN ALL N_THREADS // Wait for all threads to complete

// PRINT "Final counter value (potentially incorrect due to race condition):", GLOBAL_COUNTER

Explanation: Each thread executing increment_counter() performs the read, modify, and write steps. If thread scheduling causes these steps to interleave between multiple threads, as described in the race condition explanation, updates can be lost, leading to an incorrect final GLOBAL_COUNTER value.

2.

Consider the increment_counter function in the provided pseudo-code. Which of the following accurately describes the three steps that typically make up the 'read-modify-write' operation on GLOBAL_COUNTER that can lead to a race condition?

Select one option

Pseudo-code Fix using a Mutex (Lock)

To prevent race conditions and ensure the integrity of shared data like our global counter, synchronization mechanisms are used. A mutex (mutual exclusion lock) is a common primitive that provides exclusive access to a resource. Only one thread can hold the mutex at a time.

// Global shared variable
GLOBAL_COUNTER = 0

// Initialize a mutex (lock) for protecting the critical section
GLOBAL_MUTEX = NEW Mutex()

// Function executed by multiple threads, now safely incrementing the counter
FUNCTION increment_counter_safe():
    // Step 1: Acquire the lock. If another thread holds the lock, this thread will wait.
    GLOBAL_MUTEX.acquire()

    // CRITICAL SECTION START: This block is now protected by the mutex.
    // Only one thread can be in this section at a time.
    current_value = GLOBAL_COUNTER      // Read the current value
    current_value = current_value + 1   // Modify the value
    GLOBAL_COUNTER = current_value      // Write the new value back
    // CRITICAL SECTION END

    // Step 2: Release the lock, allowing other waiting threads to acquire it.
    GLOBAL_MUTEX.release()

    PRINT "Counter incremented to:", GLOBAL_COUNTER

// Simulate a web service request being handled by a thread
FUNCTION handle_request_safe():
    increment_counter_safe()

// Conceptual Main program flow:
// CREATE N_THREADS (e.g., 5 threads representing concurrent requests)
// FOR EACH thread IN N_THREADS:
//     START thread, executing handle_request_safe()
// JOIN ALL N_THREADS // Wait for all threads to complete

// PRINT "Final counter value (correct due to synchronization):", GLOBAL_COUNTER

Explanation of Mutex Usage:

The mutex ensures that the critical 'read-modify-write' operation on GLOBAL_COUNTER becomes atomic. When a thread calls GLOBAL_MUTEX.acquire(), it attempts to gain exclusive access to the critical section. If another thread already holds the lock, the current thread will block (pause execution) until the lock is released. Once the thread has completed the critical section, it calls GLOBAL_MUTEX.release() to free the lock, making it available for other waiting threads. This guarantees that the entire sequence of reading, incrementing, and writing the counter value happens as an indivisible unit, preventing any other thread from interfering during this process. As a result, all updates are correctly applied, and the final counter value is accurate.

3.

Explain how the GLOBAL_MUTEX.acquire() and GLOBAL_MUTEX.release() operations ensure atomicity for the read-modify-write operation on GLOBAL_COUNTER, thereby resolving the race condition.

Copyright © 2025 llmstory.comPrivacy PolicyTerms of Service