Table of Contents
KEY TAKEAWAYS
- Mutexes provide priority inheritance and are the standard way to protect shared resources in RTOS — use them instead of binary semaphores for resource protection.
- Critical sections (disabling interrupts) are fastest but should only be used for very short operations — they block all interrupts and tasks.
- Atomic operations are lock-free alternatives for simple counters, flags, and status variables — zero overhead and no risk of deadlock or priority inversion.
Part of the Complete Guide to RTOS for Embedded Systems series.
When multiple RTOS tasks access the same data or hardware, you must prevent concurrent access. Using the wrong mechanism — or no mechanism at all — causes race conditions, data corruption, and hard-to-find bugs. This article covers the three main approaches: mutexes, critical sections, and atomic operations, with clear guidance on when to use each.
The Shared Resource Problem
A race condition occurs when two tasks read-modify-write shared data and their operations interleave. Consider two tasks incrementing a shared counter:
volatile uint32_t shared_counter = 0; // volatile alone does NOT prevent races// Task A // Task B // 1. Read counter (= 5) // // 1. Read counter (= 5) // 2. Increment (= 6) // // 2. Increment (= 6) // 3. Write counter (= 6) // // 3. Write counter (= 6) // Expected: 7, Got: 6 — one increment was lost! “`
The volatile keyword prevents the compiler from optimizing away reads/writes, but it does NOT provide atomicity. The read-modify-write sequence is still three separate operations that can be interleaved by a context switch or interrupt.
Three mechanisms prevent this:
- Mutexes — one task locks, operates, unlocks. Other tasks wait.
- Critical sections — disable interrupts briefly to prevent preemption.
- Atomic operations — hardware-supported single-instruction read-modify-write.
Mutexes: The Standard Solution
A mutex (mutual exclusion) allows exactly one task to hold it at a time. Other tasks that try to take the mutex block until the holder releases it.
SemaphoreHandle_t xCounterMutex = xSemaphoreCreateMutex();void safe_increment(void) { xSemaphoreTake(xCounterMutex, portMAX_DELAY); shared_counter++; // Protected — only one task can be here xSemaphoreGive(xCounterMutex); } “`
Key mutex features:
Priority inheritance — if Task A (priority 3) holds a mutex that Task C (priority 1) is waiting for, and Task B (priority 2) is ready to run, the scheduler temporarily raises Task A to priority 3. This prevents Task B from preempting Task A and causing unbounded delay for Task C. Binary semaphores do NOT provide this — always use mutexes for resource protection.
Ownership — only the task that took the mutex can give it. This prevents accidental releases by other tasks.
Recursive mutexes — allow the same task to take the mutex multiple times (must give it the same number of times). Use when a function that takes the mutex calls another function that also takes it. Create with xSemaphoreCreateRecursiveMutex().
Mutex rules:
- Hold mutexes for the shortest possible time — long holds increase blocking and reduce concurrency
- Never hold two mutexes simultaneously if possible — this risks deadlock
- If you must hold two mutexes, always acquire them in the same order across all tasks
- Never take a mutex from an ISR — this can cause deadlock (use a queue or binary semaphore to signal a task instead)
Critical Sections: Fast but Dangerous
A critical section disables task scheduling (or interrupts) to prevent preemption. It’s the fastest protection mechanism but blocks the entire system.
// FreeRTOS critical section (disables interrupts up to configMAX_SYSCALL_INTERRUPT_PRIORITY)
taskENTER_CRITICAL();
shared_counter++; // No task or ISR can preempt here
taskEXIT_CRITICAL();// Lighter alternative — only disables task scheduling, not interrupts vTaskSuspendAll(); shared_counter++; // Tasks can’t preempt, but ISRs still fire xTaskResumeAll(); “`
When to use critical sections:
- Operations that take less than ~10 μs (a few dozen instructions)
- Protecting access that involves both tasks and ISRs (ISRs can’t take mutexes)
- Performance-critical paths where mutex overhead is unacceptable
When NOT to use:
- Any operation that might block (I/O, delays, RTOS API calls)
- Long operations — you’re disabling all task scheduling (and possibly interrupts)
- When the protected resource is only shared between tasks (use a mutex instead)
In ISRs, use the ISR-safe variant:
void TIM_IRQHandler(void) {
UBaseType_t saved = taskENTER_CRITICAL_FROM_ISR();
shared_data = new_value;
taskEXIT_CRITICAL_FROM_ISR(saved);
}Atomic Operations: Lock-Free and Overhead-Free
ARM Cortex-M3 and above provide hardware atomic instructions (LDREX/STREX) that perform read-modify-write in a single atomic operation. These are ideal for simple operations on single variables:
// GCC built-in atomics (work on ARM Cortex-M3+)
static volatile uint32_t atomic_counter = 0;void increment_counter(void) { __atomic_add_fetch(&atomic_counter, 1, __ATOMIC_SEQ_CST); }
uint32_t read_counter(void) { return __atomic_load_n(&atomic_counter, __ATOMIC_SEQ_CST); }
// Compare-and-swap for lock-free state updates bool set_state_if(uint32_t expected, uint32_t new_val) { return __atomic_compare_exchange_n(&device_state, &expected, new_val, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); } “`
Atomics are perfect for:
- Counters (event counts, error counts, statistics)
- Boolean flags (ready, busy, error states)
- Single-writer, multiple-reader status variables
Atomics do NOT replace mutexes when:
- You need to protect multiple variables that must be consistent with each other
- The critical section involves complex logic (not just a single read-modify-write)
- You need to protect hardware register access sequences
For Cortex-M0/M0+ (which lack LDREX/STREX), atomics for 32-bit values are naturally atomic (single LDR/STR instructions), but read-modify-write sequences still require disabling interrupts.
📖 Related: Volatile Keyword in C: What It Does and When to Use It • Critical Sections and Protection

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.




