Thread-safe is not reentrant, and the difference bites
The Qt documentation that started this series did not ask for a thread-safe handler. It asked for a reentrant one. Those are different requirements, and a std::mutex satisfies one without the other.
The demo has two halves. The first guards a shared counter with a std::mutex; four threads hammer it and the total comes out exactly right. That is thread-safety: concurrent calls from different threads do not corrupt shared state. The second half is a function that locks and then calls itself. It works only because the lock is a std::recursive_mutex. Swap in a plain std::mutex and the second lock() on the same thread would deadlock, because a std::mutex locked again by the thread that already owns it is undefined behavior. Thread-safe, and not reentrant.
The taxonomy, precisely
Three properties get tangled together:
- Reentrant: the function can be entered again before a prior call finishes, even on the same thread, with no ill effect. It relies on no shared mutable state and holds no non-recursive lock across the re-entry.
- Thread-safe: the function can be called concurrently from multiple threads. It often gets there by taking a lock, which is what makes it non-reentrant.
- Async-signal-safe: the function can be called from inside a signal handler.
The implication runs one way. Reentrant implies thread-safe and async-signal-safe; thread-safe implies neither. A signal handler that calls a function holding a lock the interrupted code already took will deadlock against itself, which is why the list of async-signal-safe functions is so short.
Which one a callback contract wants
When Qt says the message handler “needs to be reentrant,” it is warning that it may re-enter your handler from a nested call or a signal, not only from another thread, so a plain mutex is not automatically enough. C solved the sequence-of-writes case long ago with flockfile and funlockfile, which take a recursive per-FILE lock so a thread can hold it across several stdio calls without deadlocking itself. That recursive lock is the same shape as the recursive_mutex in the demo, and the same idea the std::print locking rules lean on.
Sources: Qt: reentrancy and thread-safety · flockfile(3) · “What constitutes asynchronous safeness”.