On Linux the following code allows the main process to shutdown its child process when a SIGINT is received (if Ctrl+C is pressed after the Wait for event message):
import multiprocessing as mp import signal import time def func(shutdown_event): signal.signal(signal.SIGINT, signal.SIG_IGN) print("Wait for event") is_set = shutdown_event.wait(timeout=10) print(f"Event outcome: {is_set}") class Main(): def run(self): self.shutdown_event = mp.Event() self.start_time = time.perf_counter() signal.signal(signal.SIGINT, self.handler) child_process = mp.Process(target=func, args=(self.shutdown_event,)) child_process.start() child_process.join(timeout=5) print(f"Exit code: {child_process.exitcode}") def handler(self, signum, frame): elapsed_time = time.perf_counter() - self.start_time print(f"Main process interrupted after {elapsed_time:.3f}s") self.shutdown_event.set() if __name__ == "__main__": mp.set_start_method("spawn") Main().run()
If the code is run on Windows the signal handler is only called once the join() times out:
Wait for event Main process interrupted after 5.0430s Exit code: None Event outcome: True
Is there a good way to handle interrupts on Windows when using multiprocessing?
Further Context I came across this because I am working on an application that uses a Manager to share data with a process running on a remote server. To keep the logs in one place I have an additional process running on the local machine that handles the log-messages. The goal is to shut everything down in the right sequence so no logs get lost.
I would first try to join as a sanity check. Maybe join in a loop with a small timeout. Your code does work on Linux.
The issue with the code cited is not related to multiprocessing; at least not directly. But I heard that on Windows you may not indeed receive signal with the parent process. I suspect, given signal.signal(signal.SIGINT, signal.SIG_IGN) in your code, you probably were receiving the event in you child process. Does not make sense to me otherwise.
The signal does reach the parent process once the blocking call releases its lock. I also tried adding a signal handler to the child process to see what happens and it leads to the same behaviour, the handler gets called once the blocking event.set() call times out.
Without the signal.signal(signal.SIGINT, signal.SIG_IGN) line a KeyboardInterrupt is raised in the child process, which leads to the main process signal handler getting called shortly after. The blocking event.wait() method gets woken up by the interrupt in this case unlike process.join() in the main process.
Wait for event Process Process-1: Traceback (most recent call last): File "C:\Users\USER\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 313, in _bootstrap self.run() ~~~~~~~~^^ File "C:\Users\USER\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\process.py", line 108, in run self._target(*self._args, **self._kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\USER\Documents\interrupt_example.py", line 8, in func is_set = shutdown_event.wait(timeout=10) File "C:\Users\USER\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\synchronize.py", line 356, in wait self._cond.wait(timeout) ~~~~~~~~~~~~~~~^^^^^^^^^ File "C:\Users\USER\AppData\Local\Programs\Python\Python313\Lib\multiprocessing\synchronize.py", line 268, in wait return self._wait_semaphore.acquire(True, timeout) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^ KeyboardInterrupt Main process interrupted after 1.605s Exit code: 1
The reason I used that line was because I’d prefer not to interrupt the child process, instead shutting it down from the main process. It feels like that might not be possible (at least on Windows).