51

I am using the new concurrent.futures module (which also has a Python 2 backport) to do some simple multithreaded I/O. I am having trouble understanding how to cleanly kill tasks started using this module.

Check out the following Python 2/3 script, which reproduces the behavior I'm seeing:

#!/usr/bin/env python
from __future__ import print_function

import concurrent.futures
import time


def control_c_this():
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        future1 = executor.submit(wait_a_bit, name="Jack")
        future2 = executor.submit(wait_a_bit, name="Jill")

        for future in concurrent.futures.as_completed([future1, future2]):
            future.result()

        print("All done!")


def wait_a_bit(name):
    print("{n} is waiting...".format(n=name))
    time.sleep(100)


if __name__ == "__main__":
    control_c_this()

While this script is running it appears impossible to kill cleanly using the regular Control-C keyboard interrupt. I am running on OS X.

  • On Python 2.7 I have to resort to kill from the command line to kill the script. Control-C is just ignored.
  • On Python 3.4, Control-C works if you hit it twice, but then a lot of strange stack traces are dumped.

Most documentation I've found online talks about how to cleanly kill threads with the old threading module. None of it seems to apply here.

And all the methods provided within the concurrent.futures module to stop stuff (like Executor.shutdown() and Future.cancel()) only work when the Futures haven't started yet or are complete, which is pointless in this case. I want to interrupt the Future immediately.

My use case is simple: When the user hits Control-C, the script should exit immediately like any well-behaved script does. That's all I want.

So what's the proper way to get this behavior when using concurrent.futures?

Nick Chammas
  • 9,813
  • 7
  • 49
  • 105
  • Reading a [related question about Java](http://stackoverflow.com/q/671049/877069), I see that killing a thread is not something you normally do because it can make your program state inconsistent. In my case I don't *think* this is a concern since I just want the whole program to exit. There was also mention of [setting some shared variable](http://stackoverflow.com/a/11387729/877069) that the threads can read to know when to self-terminate. Not sure if that approach carries over to Python. – Nick Chammas Mar 21 '15 at 03:32
  • Just a heads up, Ctrl+Break will work, even when Ctrl+C doesn't. – jedwards Mar 21 '15 at 05:14
  • @jedwards - With Python 2 I'm trying Command + . (which is Control + Break on OS X apparently) and it doesn't seem to work. Seems to be equivalent to Control + C, actually. – Nick Chammas Mar 21 '15 at 18:08

2 Answers2

34

It's kind of painful. Essentially, your worker threads have to be finished before your main thread can exit. You cannot exit unless they do. The typical workaround is to have some global state, that each thread can check to determine if they should do more work or not.

Here's the quote explaining why. In essence, if threads exited when the interpreter does, bad things could happen.

Here's a working example. Note that C-c takes at most 1 sec to propagate because the sleep duration of the child thread.

#!/usr/bin/env python
from __future__ import print_function

import concurrent.futures
import time
import sys

quit = False
def wait_a_bit(name):
    while not quit:
        print("{n} is doing work...".format(n=name))
        time.sleep(1)

def setup():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
    future1 = executor.submit(wait_a_bit, "Jack")
    future2 = executor.submit(wait_a_bit, "Jill")

    # main thread must be doing "work" to be able to catch a Ctrl+C 
    # http://www.luke.maurits.id.au/blog/post/threads-and-signals-in-python.html
    while (not (future1.done() and future2.done())):
        time.sleep(1)

if __name__ == "__main__":
    try:
        setup()
    except KeyboardInterrupt:
        quit = True
cdosborn
  • 2,314
  • 24
  • 26
  • I'll take for now that this is the best solution. It sucks that you have to have your threads burn CPU time with the sleep loop. What's more, the more responsive you want the kill to be, the shorter the sleep loop needs to be and thus, the more CPU time is burnt on it. – Nick Chammas Mar 27 '15 at 18:45
  • 1
    You don't have to sleep. You just need them to check back whether or not they should exit. – cdosborn Mar 27 '15 at 19:55
  • 1
    This trick worked for me when using `ThreadPoolExecutor` but not with `ProcessPoolExecutor`. Is there some gotcha when attempting to share global variables across processes? Would I have to store the `quit` flag on disk or something? – Gustavo Bezerra May 27 '16 at 04:15
  • 6
    Processes do not share variables, you would have to use a queue or semaphore to communicate. – mdurant Jul 27 '16 at 21:31
  • 1
    @NickChammas AFAIK sleeping doesn't usually burn CPU time in any relevant amount. Try creating 10k Threads that immediately go to sleep for one day; you won't see any CPU usage after the setup time. So in most applications this should be fine. – Felix D. Dec 06 '17 at 21:13
  • 1
    There are 2 aspects to this answer that seem missing. 1. why it is that repeatedly hitting CTRL-C will indeed shut everything down faster. 2. the quote you cite has somewhat unrealistic expectations when considering signals: "The workers could be killed while evaluating a work item, which could be bad if the callable being evaluated has external side-effects e.g. writing to a file." If the program were single-threaded, CTRL-C would generally have those implications on the execution as a whole. What about simply propagating SIGINT from the main thread to all daemons and then join()ing them? – init_js Dec 07 '18 at 17:58
4

I encountered this, but the issue I had was that many futures (10's of thousands) would be waiting to run and just pressing Ctrl-C left them waiting, not actually exiting. I was using concurrent.futures.wait to run a progress loop and needed to add a try ... except KeyboardInterrupt to handle cancelling unfinished Futures.

POLL_INTERVAL = 5
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool:
    futures = [pool.submit(do_work, arg) for arg in large_set_to_do_work_over]
    # next line returns instantly
    done, not_done = concurrent.futures.wait(futures, timeout=0)
    try:
        while not_done:
            # next line 'sleeps' this main thread, letting the thread pool run
            freshly_done, not_done = concurrent.futures.wait(not_done, timeout=POLL_INTERVAL)
            done |= freshly_done
            # more polling stats calculated here and printed every POLL_INTERVAL seconds...
    except KeyboardInterrupt:
        # only futures that are not done will prevent exiting
        for future in not_done:
            # cancel() returns False if it's already done or currently running,
            # and True if was able to cancel it; we don't need that return value
            _ = future.cancel()
         # wait for running futures that the above for loop couldn't cancel (note timeout)
         _ = concurrent.futures.wait(not_done, timeout=None)

If you're not interested in keeping exact track of what got done and what didn't (i.e. don't want a progress loop), you can replace the first wait call (the one with timeout=0) with not_done = futures and still leave the while not_done: logic.

The for future in not_done: cancel loop can probably behave differently based on that return value (or be written as a comprehension), but waiting for futures that are done or canceled isn't really waiting - it returns instantly. The last wait with timeout=None ensures that pool's running jobs really do finish.

Again, this only works correctly if the do_work that's being called actually, eventually returns within a reasonable amount of time. That was fine for me - in fact, I want to be sure that if do_work gets started, it runs to completion. If do_work is 'endless' then you'll need something like cdosborn's answer that uses a variable visible to all the threads, signaling them to stop themselves.

geitda
  • 41
  • 2