Course contentsShow
← Embedded Systems & Computer Architecture
Lesson 530 of 9367. Real-Time Operating Systems (RTOS)Free lesson

Avoiding Race Conditions in Shared Data Access

Implement atomic access patterns to prevent data corruption when multiple tasks read and write to global variables simultaneously.

In this lesson, you will learn how to implement atomic access patterns to ensure that shared global variables remain consistent when accessed by multiple concurrent tasks.

The Race Condition Problem

A race condition occurs when the final outcome of a piece of code depends on the specific timing or interleaving of multiple tasks. In embedded systems, this is common when a global variable is modified by two tasks simultaneously. Because a single line of C code (like counter++) is often compiled into multiple assembly instructions—typically a load, an increment, and a store—the scheduler can perform a context switch exactly between these instructions.

If Task A reads the value 10 and is preempted by Task B, which also reads 10 and updates it to 11, Task A will eventually resume and store its own calculation of 11, effectively "overwriting" the increment performed by Task B.

Implementing Atomic Patterns

To prevent this, you must treat the shared variable as a critical section. The simplest atomic pattern is to disable interrupts or use a mutex to ensure the entire read-modify-write cycle completes without interruption:

// Unsafe approach
shared_counter++; 

// Safe approach using a Mutex
xSemaphoreTake(xMutex, portMAX_DELAY);
shared_counter++;
xSemaphoreGive(xMutex);

For extremely simple data types (like a single boolean flag or a hardware register), you can sometimes use compiler-specific "atomic built-ins" provided by your toolchain. These tell the compiler to generate a single, non-interruptible instruction (like LDREX/STREX on ARM Cortex-M) that performs the update in hardware.

If the shared data structure is larger (such as a struct containing multiple fields), a mutex is the preferred mechanism. By wrapping the access logic, you ensure that even if the scheduler wants to switch tasks, the OS prevents it until the current task has finished updating the entire structure. This maintains data integrity and prevents your tasks from operating on "torn" data—a state where only half of a large variable has been updated.

Key Takeaway: You must protect shared global data with synchronization primitives to ensure that the read-modify-write sequence is treated as a single, uninterruptible operation.