EmbeddedRelated.com

Race Condition

Category: Rtos | Also known as: race conditions

A race condition is a defect in which the correctness of a program depends on the relative timing or interleaving of two or more concurrent operations, such as interrupts, RTOS tasks, or DMA transfers accessing shared state. Because timing varies with load, clock speed, and compiler optimizations, race conditions can cause intermittent, hard-to-reproduce failures.

In practice

In bare-metal embedded systems, the most common race condition arises between an ISR and foreground code sharing a variable. A classic example: the main loop reads a multi-byte value (say, a 32-bit counter on an 8-bit AVR or PIC) as multiple load instructions, and the ISR updates it between those loads. The result is a torn read containing a mix of old and new bytes. Declaring the variable `volatile` prevents the compiler from caching it in a register but does not make multi-step accesses atomic.

In RTOS-based designs, race conditions occur when two tasks access shared data without mutual exclusion. A task can be preempted by a higher-priority task at any point between a read and a write, leaving shared state in an inconsistent intermediate form. Mutexes, semaphores, and critical sections are the standard remedies, but each carries its own pitfalls: priority inversion with mutexes, missed signals with semaphores, and increased interrupt latency with broad critical sections.

A subtler class of race conditions involves read-modify-write sequences on hardware peripheral registers. On many Cortex-M and other MCU peripherals, a flag-clear operation requires reading a status register and writing back a modified value. If an ISR fires between the read and the write, the ISR's flag-clear may be overwritten and silently lost by the subsequent write from interrupted code. Double-buffering schemes, described in the blog post "Scorchers, Part 3: Bare-Metal Concurrency With Double-Buffering and the Revolving Fireplace," are one architectural approach to avoiding this class of problem.

Race conditions are notoriously difficult to detect by testing alone because they are timing-sensitive. A bug that appears reliably at one optimization level or clock speed may vanish at another. Static analysis tools, lock-based discipline enforced by design conventions, and formal methods such as model checking are more reliable detection strategies than stress testing alone.

Frequently asked

Does marking a variable `volatile` prevent race conditions?
No. `volatile` tells the compiler to re-read the variable from memory on every access rather than caching it in a register, which prevents a specific class of compiler optimization bug. It does not guarantee atomicity of multi-step operations. On 8-bit and 16-bit MCUs, even a single C assignment to a 32-bit variable compiles to multiple instructions, any of which can be interrupted. Proper atomicity requires either a hardware atomic instruction (where available, such as the LDREX/STREX pair on ARMv6-M and later), a critical section (interrupt disable/enable), or an RTOS primitive.
How do I protect shared data between an ISR and the main loop on a bare-metal system?
The standard approach is to disable the relevant interrupt (or all interrupts) around the access in foreground code, then re-enable it. Keep the critical section as short as possible to minimize latency impact. For simple producer-consumer patterns, a lock-free single-element or power-of-two ring buffer can avoid disabling interrupts entirely, provided the access pattern is strictly one-reader/one-writer.
What is the difference between a race condition and a data race?
The terms are often used interchangeably in embedded contexts, but in formal language specifications such as the C11 and C++11 standards, a 'data race' has a specific meaning: two threads accessing the same object concurrently where at least one access is a write and neither is protected by a synchronization operation. A data race is undefined behavior under those standards. A 'race condition' is the broader concept: any timing-dependent correctness failure, including races on hardware registers or multi-step logical operations that are not single-object writes.
Can an RTOS eliminate race conditions?
No. An RTOS provides primitives (mutexes, semaphores, message queues) that make it easier to write race-free code, but it does not enforce their use. Incorrectly applied or missing synchronization in RTOS tasks introduces exactly the same classes of race conditions as bare-metal code. The blog post 'Can an RTOS be really real-time?' discusses how RTOS scheduling decisions interact with timing guarantees, which is closely related to understanding where races can occur.
Are race conditions only a problem with multi-core processors?
No. On single-core MCUs, preemption by an ISR or by a higher-priority RTOS task creates concurrency that is sufficient to produce race conditions. Multi-core SoCs (such as dual-core STM32H7 or i.MX RT1170 parts) add true parallelism and require additional hardware synchronization mechanisms such as hardware semaphores or memory barriers, but the fundamental problem exists on any system where two execution contexts can interleave access to shared state.

Differentiators vs similar concepts

A race condition is the high-level behavioral defect: the program produces wrong results due to timing-dependent interleaving. A deadlock is a different concurrency failure mode in which two or more tasks each hold a resource the other needs, causing all of them to block forever. A priority inversion is yet another distinct failure: a high-priority task is indirectly delayed because a low-priority task holds a mutex it needs, and a medium-priority task preempts the low-priority holder. All three are concurrency bugs, but they have different root causes and different fixes.