A function is reentrant if it can be safely interrupted mid-execution and called again — by an ISR, a second RTOS task, or a recursive call — before the first invocation completes, without corrupting shared state or producing incorrect results. More precisely, reentrancy concerns any overlapping invocations from any source, not only recursive calls. Reentrancy is a property of the function's implementation, not of the calling context.
In practice
In bare-metal embedded systems, reentrancy matters wherever a function can be called from both normal (foreground) code and an ISR. A classic failure: a utility like `itoa()` or a custom ring-buffer accessor uses a static local variable to hold intermediate state. If an ISR fires mid-call and invokes the same function, both invocations write to the same static storage, silently corrupting one result. The fix is to eliminate static/global state from the function entirely, or to protect shared state with a critical section — though the latter only prevents simultaneous ISR re-entry, not recursive re-entry.
In RTOS environments, reentrancy concerns multiply because any task can call shared library functions concurrently. The C standard library is a well-known hazard: many toolchain-supplied implementations of `printf`, `strtok`, and similar functions are not reentrant by default because they use internal static buffers; `malloc` and heap-related functions share global heap state, which raises related but distinct concurrency concerns. Newlib, used on many ARM Cortex-M toolchains, provides a reentrant variant (the `_r` suffix functions, e.g., `_malloc_r`) that carries per-task state in a `struct _reent`. Whether the RTOS wires these up correctly requires explicit verification.
The simplest way to write a reentrant function is to rely exclusively on parameters and local (stack-allocated) variables, avoiding global variables, static locals, and shared hardware registers that are not independently accessed or otherwise synchronized per invocation. This aligns with the broader value of immutability and statelessness described in "Important Programming Concepts (Even on Embedded Systems) Part II: Immutability" — functions that do not modify shared state are trivially reentrant.
A common source of confusion is conflating reentrancy with thread safety. A function can be made thread-safe by wrapping shared access in a mutex, but that does not make it reentrant: if the same task re-enters a function that is already holding its own mutex, the result is deadlock. True reentrancy means each overlapping invocation operates on independent state — whether by avoiding shared state entirely or by ensuring each invocation is given its own separate state to work with.
Frequently asked
What is the difference between a reentrant function and a thread-safe function?
A reentrant function is safe to call again before a previous invocation finishes, with no shared mutable state between invocations. A thread-safe function is safe to call concurrently from multiple threads, typically by protecting shared state with a
mutex or other synchronization primitive. Thread safety via locking is not reentrant: if a task re-enters a function that already holds its own mutex, the task
deadlocks. Reentrant functions are inherently thread-safe; thread-safe functions are not necessarily reentrant.
How do I tell whether a function is reentrant?
Inspect its state. A function is likely non-reentrant if it: (1) reads or writes global or file-scope variables without atomic or locked access, (2) uses static local variables to preserve state across calls, (3) calls other non-reentrant functions (including many standard library functions), or (4) directly accesses shared hardware
registers. A function that uses only
stack-allocated locals and its parameters, and calls only other reentrant functions, is reentrant.
Is the C standard library reentrant?
Generally no, not in its default form. Functions such as `strtok`, `gmtime`, and `rand` use internal static storage.
Heap functions like `malloc` use a global data structure. Many
toolchains supply reentrant alternatives: Newlib on ARM Cortex-M provides `_r`-suffix variants (e.g., `_strtok_r`, `_malloc_r`) that take an explicit reentrant context struct. Whether your RTOS initializes a per-task context and routes calls through these variants depends on the RTOS port and toolchain configuration — check your port's documentation.
Can I make a non-reentrant function safe to use from both an ISR and foreground code by disabling interrupts?
Yes, for the specific case of
ISR re-entry. Wrapping the call site with a
critical section (disable/enable interrupts) prevents the ISR from interrupting the foreground invocation of the function, eliminating concurrent re-entry from that path. This does not make the function reentrant in the strict sense — recursive re-entry from within the same execution context is still unsafe — and it introduces interrupt
latency, which can be problematic for timing-sensitive systems.
Does recursion require reentrancy?
Yes. A recursive function calls itself before the previous invocation returns, which is a form of re-entry. For recursion to be correct, the function must be reentrant: each invocation needs its own independent state. Functions that use only
stack-allocated locals naturally satisfy this. Note that on resource-constrained MCUs with small stacks (often 256 bytes to a few kilobytes), deep recursion can cause
stack overflow even in a correctly reentrant function, so many embedded coding standards prohibit or restrict recursion regardless.
Differentiators vs similar concepts
Reentrancy is often confused with thread safety (see FAQ), and occasionally with idempotence. An idempotent function produces the same result when called multiple times with the same inputs — a property about repeated calls completing in sequence, not about overlapping calls. A function can be idempotent but non-reentrant, or reentrant but not idempotent.