Thread safe and reentrant functions

Let's start with the conclusion

Reentrant functions are not necessarily thread safe; Thread safe functions are not necessarily reentrant.

Concept of reentrant function

Reentrant programs (functions) are allowed to be interrupted during execution and called safely again in the interrupted code.
If a function is reentrant, the function should meet the following conditions:

  • Cannot contain static (global) non constant data.
  • The address of static (global) non constant data cannot be returned.
  • Only data provided by the caller can be processed.
  • Locks that cannot depend on single instance mode resources.
  • The function calling must also be reentrant.

In a word: the above condition requires that all variables used by the reentrant function be provided through the caller of the function.
For example, design a state machine for sending strings:

fsm_rt_t print_str(const char *pchStr)
{
    static enum {
        START = 0,
        IS_END_OF_STRING,
        SEND_CHAR,
    } s_tState = START;
    static const char *s_pchStr = NULL;

    switch (s_tState) {
        case START:
            s_pchStr = pchStr;
            s_tState = IS_END_OF_STRING;
            //break;    //!< fall-through
        case IS_END_OF_STRING:
            if (*s_pchStr == '\0') {
                PRINT_STR_RESET_FSM();
                return fsm_rt_cpl;
            }
            s_tState = SEND_CHAR;
            //break;    //!< fall-through
        case SEND_CHAR:
            if (serial_out(*s_pchStr)) {
                pchStr++;
                s_tState = IS_END_OF_STRING;
            }
            break;
    }

    return fsm_rt_on_going;
}

Because static variables are used in the design of state machine, especially the state variable s_tState -- this means multiple prints executed at the same time_ STR, which share the same state variable with each other, interfere with each other. This means that multiple prints are executed at the same time_ STR is "unsafe" and can cause problems (for example, buffer overflow may occur when the string length is inconsistent). Therefore, print can be said_ STR is not reentrant.

After changing the code:

#undef this
#define this (*ptThis)

#define PRINT_STR_RESET_FSM()               \
        do { this.State = START; } while(0)

typedef struct print_str_t {
    uint8_t chState;    
    const char *pchStr;  
} print_str_t;
fsm_rt_t print_str(print_str_t *ptThis, const char *pchStr)
{
    enum {
        START = 0,
        IS_END_OF_STRING,
        SEND_CHAR,
    };

    switch (this.chState) {
        case START:
            this.pchStr = pchStr;
            this.chState = IS_END_OF_STRING;
            //break;    //!< fall-through
        case IS_END_OF_STRING:
            if (*(this.pchStr) == '\0') {
                PRINT_STR_RESET_FSM();
                return fsm_rt_cpl;
            }
            this.chState = SEND_CHAR;
            //break;    //!< fall-through
        case SEND_CHAR:
            if (serial_out(*(this.pchStr))) {
                this.pchStr++;
                this.chState = IS_END_OF_STRING;
            }
            break;
    }

    return fsm_rt_on_going;
}

All variables used by this state machine have a state machine control block print_str_t is provided, that is, provided by the caller of the function, so it is reentrant.
State machine print_str uses the shared function serial_out(), which is a critical resource. When there are multiple instances of this state machine, it will inevitably access this critical resource, resulting in the printed data not being what you want, so this state machine is not thread safe.

Thread safety concept

Thread safety means that when a function or function library is called in a multithreaded environment, it can correctly handle the shared resources among multiple threads and make the program function complete correctly.
For example:

#include <pthread.h>

int increment_counter ()
{
	static int counter = 0;
	static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

	pthread_mutex_lock(&mutex);
	
	// only allow one thread to increment at a time
	++counter;
	// store value before any other threads increment it further
	int result = counter;	

	pthread_mutex_unlock(&mutex);
	
	return result;
}

In the above code, the function increment_counter can be called in multiple threads because there is a mutex to synchronize access to the shared variable counter. But if this function is used in a reentrant interrupt handler, if in pthread_ mutex_ Lock (& mutex) and pthread_ mutex_ Another calling function increment is generated between unlock (& mutex)_ If counter is interrupted, this function will be executed for the second time. At this time, because mutex has been locked, the function will be in pthread_ mutex_ Block at lock (& mutex), and since mutex has no chance to be unlocked, the block will last forever. In short, the problem is that the mutex of pthread is not reentrant.

The relationship between the two

Both reentrant and thread safety concepts are related to the way functions handle resources. However, they differ significantly:

  1. Reentrant is a concept in single thread design, and thread safety is a concept in multi thread design.
  • The reentrant function may be reentrant by the same execution thread for its own reasons, such as jmp or call, or interrupt response. Reentrant emphasizes that it is still safe to re-enter the same subroutine (function) when executing a single thread.
  • Thread safe functions need to treat the resources shared by multiple threads correctly. If there are resources shared by multiple threads in a function, protect the data, such as locking.
  • From the perspective of face objects, reentrant functions are single instance calls, and thread safety is multi instance calls.
  1. The concept of reentrant will affect the external interface of the function, while thread safety only cares about the implementation of the function.
  • In most cases, to change a non reentrant function into a reentrant function, you need to modify the function interface so that all data is provided through the caller of the function.
  • To change a non thread safe function to thread safe, you only need to modify the implementation part of the function. Generally, the synchronization mechanism is added to protect the shared resources from being accessed by several threads at the same time.

Posted by mark123$ on Sun, 26 Sep 2021 10:40:34 -0700