Python multiprocessing – catch SIGINT/SIGTERM and exit gracefully

Question:

I have two python scripts and I want them to communicate to each other. Specifically, I want script Communication.py to send an array to script Process.py if required by the latter. I’ve used module multiprocessing.Process and multiprocessing.Pipe to make it works. My code works, but I want to handle gracefully SIGINT and SIGTERM, I’ve tried the following but it does not exit gracefully:

Process.py

from multiprocessing import Process, Pipe
from Communication import arraySender
import time
import signal

class GracefulKiller:
    kill_now = False
    def __init__(self):
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)

    def exit_gracefully(self, *args):
        self.kill_now = True

def main():
    parent_conn, child_conn = Pipe()
    p = Process(target=arraySender, args=(child_conn,True))
    p.start()
    print(parent_conn.recv())

if __name__ == '__main__':
  killer = GracefulKiller()
  while not killer.kill_now:
      main()

Communication.py

import numpy
from multiprocessing import Process, Pipe

def arraySender(child_conn, sendData):
    if sendData:
        child_conn.send(numpy.random.randint(0, high=10, size=15, dtype=int))
        child_conn.close()

what am I doing wrong?

Asked By: Samuele Mecenero

||

Answers:

I strongly suspect you are running this under Windows because I think the code you have should work under Linux. This is why it is important to always tag your questions concerning Python and multiprocessing with the actual platform you are on.

The problem appears to be due to the fact that in addition to your main process you have created a child process in function main that is also receiving the signals. The solution would normally be to add calls like signal.signal(signal.SIGINT, signal.SIG_IGN) to your array_sender worker function. But there are two problems with this:

  1. There is a race condition: The signal could be received by the child process before it has a change to ignore signals.
  2. Regardless, the call to ignore signals when you are using multiprocess.Processing does not seem to work (perhaps that class does its own signal handling that overrides these calls).

The solution is to create a multiprocessing pool and initialize each pool process so that they ignore signals before you submit any tasks. The other advantage of using a pool, although in this case we only need a pool size of 1 because you never have more than one task running at a time, is that you only need to create the process once which can then be reused.

As an aside, you have some inconsistency in your GracefulKiller class by mixing a class attribute kill_now with an instance attribute kill_now that gets created when you execute self.kill_now = True. So when the main process is testing killer.kill_now it is accessing the class attribute until such time as self.kill_now is set to True when it will then be accessing the instance attribute.

from multiprocessing import Pool, Pipe
import time
import signal
import numpy

class GracefulKiller:
    def __init__(self):
        self.kill_now = False # Instance attribute
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)

    def exit_gracefully(self, *args):
        self.kill_now = True


def init_pool_processes():
    signal.signal(signal.SIGINT, signal.SIG_IGN)
    signal.signal(signal.SIGTERM, signal.SIG_IGN)

def arraySender(sendData):
    if sendData:
        return numpy.random.randint(0, high=10, size=15, dtype=int)

def main(pool):
    result = pool.apply(arraySender, args=(True,))
    print(result)

if __name__ == '__main__':
    # Create pool with only 1 process:
    pool = Pool(1, initializer=init_pool_processes)
    killer = GracefulKiller()
    while not killer.kill_now:
        main(pool)
    pool.close()
    pool.join()

Ideally GracefulKiller should be a singleton class so that regardless of how many times GracefulKiller was instantiated by a process, you would be calling signal.signal only once for each type of signal you want to handle:

class Singleton(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super().__call__(*args, **kwargs)
        return self.__instance

class GracefulKiller(metaclass=Singleton):
    def __init__(self):
        self.kill_now = False # Instance attribute
        signal.signal(signal.SIGINT, self.exit_gracefully)
        signal.signal(signal.SIGTERM, self.exit_gracefully)

    def exit_gracefully(self, *args):
        self.kill_now = True
Answered By: Booboo