5

I've got the following code which uses a concurrent.futures.ThreadPoolExecutor to launch processes of another program in a metered way (no more than 30 at a time). I additionally want the ability to stop all work if I ctrl-C the python process. This code works with one caveat: I have to ctrl-C twice. The first time I send the SIGINT, nothing happens; the second time, I see the "sending SIGKILL to processes", the processes die, and it works. What is happening to my first SIGINT?

execution_list = [['prog', 'arg1'], ['prog', 'arg2']] ... etc
processes = []

def launch_instance(args):
    process = subprocess.Popen(args)
    processes.append(process)
    process.wait()

try:
    with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
        results = list(executor.map(launch_instance, execution_list))
except KeyboardInterrupt:
    print('sending SIGKILL to processes')
    for p in processes:
        if p.poll() is None: #If process is still alive
            p.send_signal(signal.SIGKILL)
sheridp
  • 1,386
  • 1
  • 11
  • 24

1 Answers1

1

I stumbled upon your question while trying to solve something similar. Not 100% sure that it will solve your use case (I'm not using subprocesses), but I think it will.

Your code will stay within the context manager of the executor as long as the jobs are still running. My educated guess is that the first KeyboardInterrupt will be caught by the ThreadPoolExecutor, whose default behaviour would be to not start any new jobs, wait until the current ones are finished, and then clean up (and probably reraise the KeyboardInterrupt). But the processes are probably long running, so you wouldn't notice. The second KeyboardInterrupt then interrupts this error handling.

How I solved my problem (inifinite background processes in separate threads) is with the following code:

from concurrent.futures import ThreadPoolExecutor
import signal
import threading
from time import sleep


def loop_worker(exiting):
    while not exiting.is_set():
        try:
            print("started work")
            sleep(10)
            print("finished work")
        except KeyboardInterrupt:
            print("caught keyboardinterrupt")  # never caught here. just for demonstration purposes


def loop_in_worker():
    exiting = threading.Event()
    def signal_handler(signum, frame):
        print("Setting exiting event")
        exiting.set()

    signal.signal(signal.SIGTERM, signal_handler)
    with ThreadPoolExecutor(max_workers=1) as executor:
        executor.submit(loop_worker, exiting)

        try:
            while not exiting.is_set():
                sleep(1)
                print('waiting')
        except KeyboardInterrupt:
            print('Caught keyboardinterrupt')
            exiting.set()
    print("Main thread finished (and thus all others)")


if __name__ == '__main__':
    loop_in_worker()

It uses an Event to signal to the threads that they should stop what they are doing. In the main loop, there is a loop just to keep busy and check for any exceptions. Note that this loop is within the context of the ThreadPoolExecutor.

As a bonus it also handles the SIGTERM signal by using the same exiting Event.

If you add a loop in between processes.append(process) and process.wait() that checks for a signal, then it will probably solve your use case as well. It depends on what you want to do with the running processes what actions you should take there.

If you run my script from the command line and press ctrl-C you should see something like:

started work
waiting
waiting
^CCaught keyboardinterrupt

   # some time passes here

finished work
Main thread finished (and thus all others)

Inspiration for my solution came from this blog post

FlorianK
  • 400
  • 2
  • 14
  • I have a use case, where I hit API in threadpoolexecutor, even on pressing Ctrl+C twice..It takes time to get the results of APi requests already made and then it stops. Is there any way to immediately exit and not wait even for already made requests ? Also, is this a issue which should be raised upstream ? – Simplecode Sep 28 '21 at 10:34
  • Hi Simplecode. I don't think the stuff described here are issues, but rather are like this by design. So raising an issue upstream is probably not what's needed here. Regarding your question: I'm not sure, but I think that if you post a separate question with more details regarding your setup and what you've tried, someone will be able to answer it. The more complete the code of what you tried, the easier it is for someone with more experience to see what's going on and can be done with it. Best, FlorianK – FlorianK Oct 04 '21 at 17:01
  • It doesn't work in Win10, python3.7 – James Jun 07 '22 at 10:30
  • Hi @James, I can't test it (no Windows machine here), but perhaps you could try using signal.SIGINT or signal.CTRL_BREAK_EVENT instead if signal.SIGTERM – FlorianK Jun 08 '22 at 11:20