Programming

What exactly is a reentrant function

19 September 2026 · 15 min read

What exactly is a reentrant function

Understanding concurrent programming can be tricky, especially when dealing with shared resources. One concept that surfaces frequently in this context is that of a reentrant function. So, what exactly is a reentrant function? In essence, a reentrant function is one that can be safely called simultaneously from multiple threads or processes without causing data corruption or unexpected behavior. This characteristic is crucial in multithreaded applications, operating systems, and embedded systems, where multiple execution contexts might try to access the same function at the same time. Failing to ensure reentrancy can lead to race conditions, deadlocks, and other subtle and difficult-to-debug errors. In this article, we’ll delve into the definition of reentrant functions, explore the conditions they must satisfy, provide practical examples, and discuss strategies for writing reentrant code to enhance the robustness and reliability of your software. The importance of reentrancy cannot be overstated when designing systems for concurrent execution.

Defining Reentrancy: What Makes a Function Reentrant?

A function is considered reentrant if it adheres to a specific set of rules that guarantee its safe execution in a concurrent environment. The core principle is that a reentrant function must not hold any static or global data that could be modified by another invocation of the same function. This prevents one thread from corrupting the data being used by another thread. In other words, each invocation of the function should operate on its own private data or data passed explicitly as arguments.

Furthermore, a reentrant function must not call any other non-reentrant functions. If a function calls another function that relies on global state or static variables, it inherits the non-reentrant nature of that called function. This requirement extends transitively; all functions in the call chain must be reentrant. Properly designed reentrant functions contribute significantly to the stability and predictability of multithreaded applications. According to a study by the National Institute of Standards and Technology (NIST), race conditions, often caused by non-reentrant code, are among the most prevalent and challenging bugs in concurrent systems. NIST Website.

For a function to be truly reentrant, it needs to rely exclusively on the following:

  • Function parameters passed by value or by reference to thread-local data.
  • Locally declared variables within the function’s scope (stack variables).
  • Other reentrant functions.

Understanding the Conditions for Reentrancy

Several key conditions must be met for a function to be considered reentrant. Failing to adhere to these conditions can lead to concurrency-related issues that are notoriously difficult to diagnose. One primary condition is the avoidance of global variables or static variables. These shared resources can be modified by one thread while another thread is in the middle of executing the same function, leading to unpredictable results. Instead, functions should rely on parameters passed to them or local variables created within their scope.

Another crucial condition is the avoidance of non-reentrant functions. If a function calls another function that is not reentrant, it becomes non-reentrant itself. This is because the original function is now indirectly relying on shared resources that could be modified by other threads. Therefore, it’s essential to ensure that all functions in the call chain are reentrant. Finally, reentrant functions should avoid using shared resources like files or network connections without proper synchronization mechanisms (e.g., mutexes). However, using mutexes can introduce the risk of deadlocks, so it’s generally preferable to design functions that don’t require them at all.

Featured Snippet: Reentrant functions must avoid using global or static variables. These shared resources can be modified by one thread while another is executing the same function, leading to unpredictable outcomes. Relying instead on parameters or local variables is crucial. Additionally, reentrant functions must not call non-reentrant functions, as this can indirectly introduce shared resource dependencies.

Examples of Reentrant and Non-Reentrant Functions

To better understand the concept, let’s look at some examples. A simple example of a reentrant function is one that performs a calculation based solely on its input parameters and local variables, such as calculating the factorial of a number. Each call to the function operates independently, without relying on any shared state. Consider the following C example:

int factorial(int n) { if (n <= 1) { return 1; } else { return n  factorial(n - 1); } } 

This function is reentrant because it only uses the input parameter n and local variables for its computation. No global or static variables are involved. On the other hand, a non-reentrant function might use a global variable to store intermediate results. For example:

int result; // Global variable int nonReentrantFunc(int x) { result = x  2; return result; } 

This function is non-reentrant because multiple threads calling it simultaneously could interfere with each other’s calculations by modifying the global result variable. Another example is a function that uses a static buffer to store data. This buffer could be overwritten by another thread, leading to data corruption. These examples highlight the importance of carefully considering how functions interact with shared resources when designing concurrent systems. According to a study published in IEEE Transactions on Software Engineering, non-reentrant code is a major source of bugs in multithreaded applications. IEEE Website.

Strategies for Writing Reentrant Code

Writing reentrant code requires careful attention to detail and a thorough understanding of concurrent programming principles. One of the most effective strategies is to avoid global and static variables altogether. Instead, pass all necessary data as parameters to the function. This ensures that each call to the function operates on its own private copy of the data. When dealing with shared resources, such as files or network connections, use synchronization mechanisms like mutexes or semaphores to protect access. However, be mindful of the potential for deadlocks and other concurrency issues when using these mechanisms.

Another strategy is to use thread-local storage. Thread-local storage provides each thread with its own private copy of a variable, eliminating the need for synchronization. This can be a more efficient approach than using mutexes in some cases. Additionally, carefully review all function calls to ensure that they are reentrant. If a function calls a non-reentrant function, it becomes non-reentrant itself. Consider using reentrant alternatives to standard library functions, such as the _r versions of some C standard library functions (e.g., strtok_r instead of strtok). Finally, thoroughly test your code in a concurrent environment to identify and fix any reentrancy issues. Tools like thread sanitizers can help detect race conditions and other concurrency-related bugs.

  1. Avoid global and static variables.
  2. Pass all necessary data as parameters.
  3. Use thread-local storage when appropriate.
  4. Protect shared resources with synchronization mechanisms.
  5. Ensure all function calls are reentrant.
  6. Thoroughly test your code in a concurrent environment.

Practical Applications and Considerations

Reentrant functions are essential in various applications, especially those involving concurrency and parallelism. Operating systems, for example, heavily rely on reentrant functions to handle interrupts and system calls safely. Interrupt handlers must be reentrant to avoid corrupting data if an interrupt occurs while the handler is already executing. Similarly, system calls, which are often invoked by multiple processes simultaneously, must be reentrant to ensure data integrity.

In embedded systems, where resources are often limited, reentrant functions are crucial for efficient and reliable operation. Real-time operating systems (RTOS) used in embedded systems often rely on reentrant code to manage tasks and resources. Server applications, such as web servers and database servers, also benefit from reentrant functions. These applications typically handle multiple client requests concurrently, and reentrant code ensures that each request is processed independently without interfering with others. When designing systems that require high levels of concurrency and reliability, the design patterns used to facilitate safe concurrent operations should be considered. For example, the Single Responsibility Principle can help ensure that functions are focused and less likely to inadvertently modify shared state. Learn more about concurrent programming. Always prioritize reentrancy in your code.

Infographic here
FAQ About Reentrant Functions -----------------------------
What is the difference between reentrant and thread-safe?
While related, reentrant and thread-safe are not synonymous. A reentrant function can be safely called by multiple threads concurrently without relying on explicit locking mechanisms. A thread-safe function, on the other hand, may use locking mechanisms (like mutexes) to protect shared resources and ensure safe concurrent access. All reentrant functions are thread-safe, but not all thread-safe functions are reentrant.
Why is reentrancy important in signal handlers?
Signal handlers can interrupt a program at any point, even in the middle of another function. If the signal handler calls a non-reentrant function, it could corrupt the state of the interrupted function. Therefore, signal handlers must only call reentrant functions to avoid this issue.
Can a function with memory allocation be reentrant?
Yes, a function with memory allocation can be reentrant if the memory is allocated from a thread-local heap or managed in a way that ensures each thread has its own private memory space. Using thread-safe memory allocators is crucial in such cases.
Understanding and implementing reentrant functions is a cornerstone of robust and reliable concurrent programming. By adhering to the principles of avoiding shared state, using thread-local storage when needed, and carefully reviewing function calls, you can build systems that gracefully handle concurrent execution without the headaches of race conditions and data corruption. Always prioritize reentrancy when designing code for multithreaded environments. It's not just about making the code work; it's about making it work correctly and consistently under pressure.

Ready to take your concurrent programming skills to the next level? Explore advanced synchronization techniques like semaphores and condition variables. Dive deeper into thread-local storage and its practical applications. And don’t forget to continuously test and refine your code to ensure it remains reentrant and reliable as your system evolves. Check out the POSIX standard for more information on thread-safe functions. POSIX Standard.

Question & Answer :
Most of the times, the definition of reentrance is quoted from Wikipedia:

A computer program or routine is described as reentrant if it can be safely called again before its previous invocation has been completed (i.e it can be safely executed concurrently). To be reentrant, a computer program or routine:

  1. Must hold no static (or global) non-constant data.
  2. Must not return the address to static (or global) non-constant data.
  3. Must work only on the data provided to it by the caller.
  4. Must not rely on locks to singleton resources.
  5. Must not modify its own code (unless executing in its own unique thread storage)
  6. Must not call non-reentrant computer programs or routines.

How is safely defined?

If a program can be safely executed concurrently, does it always mean that it is reentrant?

What exactly is the common thread between the six points mentioned that I should keep in mind while checking my code for reentrant capabilities?

Also,

  1. Are all recursive functions reentrant?
  2. Are all thread-safe functions reentrant?
  3. Are all recursive and thread-safe functions reentrant?

While writing this question, one thing comes to mind: Are the terms like reentrance and thread safety absolute at all i.e. do they have fixed concrete definitions? For, if they are not, this question is not very meaningful.

  1. How is safely defined?

Semantically. In this case, this is not a hard-defined term. It just mean “You can do that, without risk”.

  1. If a program can be safely executed concurrently, does it always mean that it is reentrant?

No.

For example, let’s have a C++ function that takes both a lock, and a callback as a parameter:

#include <mutex> typedef void (*callback)(); std::mutex m; void foo(callback f) { m.lock(); // use the resource protected by the mutex if (f) { f(); } // use the resource protected by the mutex m.unlock(); } 

Another function could well need to lock the same mutex:

void bar() { foo(nullptr); } 

At first sight, everything seems ok… But wait:

int main() { foo(bar); return 0; } 

If the lock on mutex is not recursive, then here’s what will happen, in the main thread:

  1. main will call foo.
  2. foo will acquire the lock.
  3. foo will call bar, which will call foo.
  4. the 2nd foo will try to acquire the lock, fail and wait for it to be released.
  5. Deadlock.
  6. Oops…

Ok, I cheated, using the callback thing. But it’s easy to imagine more complex pieces of code having a similar effect.

  1. What exactly is the common thread between the six points mentioned that I should keep in mind while checking my code for reentrant capabilities?

You can smell a problem if your function has/gives access to a modifiable persistent resource, or has/gives access to a function that smells.

(Ok, 99% of our code should smell, then… See last section to handle that…)

So, studying your code, one of those points should alert you:

  1. The function has a state (i.e. access a global variable, or even a data member)
  2. This function can be called by multiple threads, or could appear twice in the stack while the process is executing (i.e. the function could call itself, directly or indirectly). Function taking callbacks as parameters smell a lot.

Note that non-reentrancy is viral : A function that could call a possible non-reentrant function cannot be considered reentrant.

Note, too, that C++ methods smell because they have access to this, so you should study the code to be sure they have no funny interaction.

4.1. Are all recursive functions reentrant?

No.

In multithreaded cases, a recursive function accessing a shared resource could be called by multiple threads at the same moment, resulting in bad/corrupted data.

In singlethreaded cases, a recursive function could use a non-reentrant function (like the infamous strtok), or use global data without handling the fact the data is already in use. So your function is recursive because it calls itself directly or indirectly, but it can still be recursive-unsafe.

4.2. Are all thread-safe functions reentrant?

In the example above, I showed how an apparently threadsafe function was not reentrant. OK, I cheated because of the callback parameter. But then, there are multiple ways to deadlock a thread by having it acquire twice a non-recursive lock.

4.3. Are all recursive and thread-safe functions reentrant?

I would say “yes” if by “recursive” you mean “recursive-safe”.

If you can guarantee that a function can be called simultaneously by multiple threads, and can call itself, directly or indirectly, without problems, then it is reentrant.

The problem is evaluating this guarantee… ^_^

  1. Are the terms like reentrance and thread safety absolute at all, i.e. do they have fixed concrete definitions?

I believe they do, but then, evaluating a function is thread-safe or reentrant can be difficult. This is why I used the term smell above: You can find a function is not reentrant, but it could be difficult to be sure a complex piece of code is reentrant

  1. An example

Let’s say you have an object, with one method that needs to use a resource:

struct MyStruct { P * p; void foo() { if (this->p == nullptr) { this->p = new P(); } // lots of code, some using this->p if (this->p != nullptr) { delete this->p; this->p = nullptr; } } }; 

The first problem is that if somehow this function is called recursively (i.e. this function calls itself, directly or indirectly), the code will probably crash, because this->p will be deleted at the end of the last call, and still probably be used before the end of the first call.

Thus, this code is not recursive-safe.

We could use a reference counter to correct this:

struct MyStruct { size_t c; P * p; void foo() { if (c == 0) { this->p = new P(); } ++c; // lots of code, some using this->p --c; if (c == 0) { delete this->p; this->p = nullptr; } } }; 

This way, the code becomes recursive-safe… But it is still not reentrant because of multithreading issues: We must be sure the modifications of c and of p will be done atomically, using a recursive mutex (not all mutexes are recursive):

#include <mutex> struct MyStruct { std::recursive_mutex m; size_t c; P * p; void foo() { m.lock(); if (c == 0) { this->p = new P(); } ++c; m.unlock(); // lots of code, some using this->p m.lock(); --c; if (c == 0) { delete this->p; this->p = nullptr; } m.unlock(); } }; 

And of course, this all assumes the lots of code is itself reentrant, including the use of p.

And the code above is not even remotely exception-safe, but this is another story… ^_^

  1. Hey 99% of our code is not reentrant!

It is quite true for spaghetti code. But if you partition correctly your code, you will avoid reentrancy problems.

7.1. Make sure all functions have NO state

They must only use the parameters, their own local variables, other functions without state, and return copies of the data if they return at all.

7.2. Make sure your object is “recursive-safe”

An object method has access to this, so it shares a state with all the methods of the same instance of the object.

So, make sure the object can be used at one point in the stack (i.e. calling method A), and then, at another point (i.e. calling method B), without corrupting the whole object. Design your object to make sure that upon exiting a method, the object is stable and correct (no dangling pointers, no contradicting data members, etc.).

7.3. Make sure all your objects are correctly encapsulated

No one else should have access to their internal data:

// bad int & MyObject::getCounter() { return this->counter; } // good int MyObject::getCounter() { return this->counter; } // good, too void MyObject::getCounter(int & p_counter) { p_counter = this->counter; } 

Even returning a const reference could be dangerous if the user retrieves the address of the data, as some other portion of the code could modify it without the code holding the const reference being told.

7.4. Make sure the user knows your object is not thread-safe

Thus, the user is responsible to use mutexes to use an object shared between threads.

The objects from the STL are designed to be not thread-safe (because of performance issues), and thus, if a user want to share a std::string between two threads, the user must protect its access with concurrency primitives;

7.5. Make sure your thread-safe code is recursive-safe

This means using recursive mutexes if you believe the same resource can be used twice by the same thread.