`printf` is non-async-signal-safe because, as you describe, it ends up manipulating global state without synchronisation. For added fun, it’s not necessarily re-entrant. In your example, the signal might be handled while the first `printf` is running, and the second `printf` could mess up the state of the first call.
The recommended async-signal-safe approach is for the signal handler to set a flag somewhere, and have the main program flow deal with the flag. This avoids issues with re-entrance, serialises output, and helps keep signal handlers speedy.