In embedded systems, there is a dangerous trap where standard thread safety fails completely, leaving your application permanently frozen. That trap is to use a mutex inside an interrupt handler. Consider this thread-safe function, where you protect shared state with a mutex:
void update_hardware(int data) {
pthread_mutex_lock(&lock)
global_hardware_buffer = data; // What if an interrupt hits right here?
pthread_mutex_unlock(&lock);
}
If this function is called by an interrupt service routine (ISR) and a hardware interrupt fires right in the middle of that function:
- The Main Thread acquires the lock.
- The Interrupt hits. The CPU immediately freezes the main thread and jumps to your ISR.
- The Re-entry: The interrupt handler needs to log something, so it calls update_hardware().
- The Deadlock: The interrupt handler hits pthread_mutex_lock(). It sees the lock is busy, so it waits.
But who is it waiting for? It's waiting for the main thread to release the lock. Except the main thread is frozen underneath the interrupt handler, waiting for the interrupt to finish!
A common misconception is that the operating system's scheduler will see the interrupt handler is blocked, context-switch it out, let the main thread finish, and fix the mess. It can't. In embedded systems or Real-Time Operating Systems (RTOS), interrupts run at a higher execution priority than the scheduler itself. ISRs execute outside normal thread scheduling. If an ISR attempts to wait for a resource held by the interrupted thread, forward progress becomes impossible because the interrupted thread cannot run until the ISR completes. This is priority inversion taken to the extreme. The ISR (highest priority in the system) ends up waiting for a lower-priority thread that it itself has preempted.
To survive interrupts, your code cannot just be thread-safe, it must be reentrant. A reentrant function is completely self-contained. It never touches global variables, it never uses static buffers, and it never locks a mutex. It relies strictly on local variables allocated on the stack or parameters passed to it. Because it has no shared memory between calls, it can be interrupted at any instruction and safely called again without data corruption or deadlocks. Here is a reentrant function example:
// This function operates purely on local stack memory.
// It can be safely interrupted and re-entered at any microsecond.
int calculate_hardware_state(int current_state, int new_data) {
int next_state;
next_state = current_state + new_data;
return next_state;
}
Also pay attention to macros because if a macro references a global variable, a static variable, or a hardcoded hardware register under the hood, any function using that macro instantly becomes non-reentrant:
int global_status = 0; #define SET_STATUS_FLAG(mask) (global_status |= (mask))
Never use blocking synchronization primitives (like mutexes, malloc, or I/O) inside an interrupt handler or signal handler. If you must pass data between an interrupt and your main loop, stick to lock-free mechanisms like C11 atomics or volatile flags.