73

How to make a multi-thread python program response to Ctrl+C key event?

Edit: The code is like this:

import threading
current = 0

class MyThread(threading.Thread):
    def __init__(self, total):
        threading.Thread.__init__(self)
        self.total = total

    def stop(self):
        self._Thread__stop()

    def run(self):
        global current
        while current<self.total:
            lock = threading.Lock()
            lock.acquire()
            current+=1
            lock.release()
            print current

if __name__=='__main__':

    threads = []
    thread_count = 10
    total = 10000
    for i in range(0, thread_count):
        t = MyThread(total)
        t.setDaemon(True)
        threads.append(t)
    for i in range(0, thread_count):
        threads[i].start()

I tried to remove join() on all threads but it still doesn't work. Is it because the lock segment inside each thread's run() procedure?

Edit: The above code is supposed to work but it always interrupted when current variable was in 5,000-6,000 range and through out the errors as below

Exception in thread Thread-4 (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "/usr/lib/python2.5/threading.py", line 486, in __bootstrap_inner
  File "test.py", line 20, in run
<type 'exceptions.TypeError'>: unsupported operand type(s) for +=: 'NoneType' and 'int'
Exception in thread Thread-2 (most likely raised during interpreter shutdown):
Traceback (most recent call last):
  File "/usr/lib/python2.5/threading.py", line 486, in __bootstrap_inner
  File "test.py", line 22, in run
jack
  • 15,121
  • 32
  • 94
  • 122
  • FWIW, I'm running into [this same issue but with the newer `concurrent.futures` module](http://stackoverflow.com/q/29177490/877069). Still trying to figure out if or how any of the solutions here translate from `threading` to `concurrent.futures`. – Nick Chammas Mar 23 '15 at 15:36

7 Answers7

93

Make every thread except the main one a daemon (t.daemon = True in 2.6 or better, t.setDaemon(True) in 2.6 or less, for every thread object t before you start it). That way, when the main thread receives the KeyboardInterrupt, if it doesn't catch it or catches it but decided to terminate anyway, the whole process will terminate. See the docs.

edit: having just seen the OP's code (not originally posted) and the claim that "it doesn't work", it appears I have to add...:

Of course, if you want your main thread to stay responsive (e.g. to control-C), don't mire it into blocking calls, such as joining another thread -- especially not totally useless blocking calls, such as joining daemon threads. For example, just change the final loop in the main thread from the current (utterless and damaging):

for i in range(0, thread_count):
    threads[i].join()

to something more sensible like:

while threading.active_count() > 0:
    time.sleep(0.1)

if your main has nothing better to do than either for all threads to terminate on their own, or for a control-C (or other signal) to be received.

Of course, there are many other usable patterns if you'd rather have your threads not terminate abruptly (as daemonic threads may) -- unless they, too, are mired forever in unconditionally-blocking calls, deadlocks, and the like;-).

Alex Martelli
  • 762,786
  • 156
  • 1,160
  • 1,345
  • Ah, you're making a (useless) blocking call -- so of course there's no response to control-C. The solution is pretty simple: just **don't** make useless blocking calls if you want to stay responsive. Let me edit my answer to explain. – Alex Martelli Oct 28 '09 at 05:06
  • i supplemented more code in original post. I tried your method, it can detect KeyboardInterrupt event but main program just doesn't quit. Is it caused by the lock segment inside each thread's run() procedure? – jack Oct 28 '09 at 05:52
  • @Alex: `Thread.join()` has optional `timeout` parameter. Isn't it better than `sleep()`? – Denis Otkidach Oct 28 '09 at 08:35
  • 2
    @jack, the code you now give terminates immediately (of course, because the main thread "falls off the end" and that ends everything). So it can't be at all close to what you're actually trying and it's clear you haven't even tried the code you're posting; makes it really hard to help you! Please post code that you HAVE tried and DOES reproduce the issue you're having, so I can show you where your bug is in that code, instead of trying to guess what bugs your unseen code might perhaps have. – Alex Martelli Oct 28 '09 at 14:54
  • @Denis, in the code I posted, the time.sleep calls in main are just whiling time away because by assumption there's nothing to do but twiddle thumbs until all thread terminate or a control=C arrives. a t.join with a very short timeout would just be a somewhat more expensive way to twiddle thumbs, one with a long timeout would hurt responsitivity to control=C, so, what's the point? – Alex Martelli Oct 28 '09 at 14:55
  • @Alex, I did tried the code, the threads were even unable to complete. I appended the errors it through out. – jack Oct 29 '09 at 07:06
  • 2
    @jack, as I already mentioned, if that's your code then your main bug now is that "the main thread "falls off the end" and that ends everything". Try the code I've already given above: `while threading.active_count() > 0: time.sleep(0.1)` -- why make me repeat this?! You can do better (the `while` in the thread should `and` a global flag so it can be stopped cleanly) but you have pther horrible bugs to fix first: they include acquire/release on a lock that's a new local variable, which is just the same as no locking, instead of a lock shared by all threads. – Alex Martelli Oct 29 '09 at 14:51
  • 1
    @AlexMartelli About the question `time.sleep()` vs `time.join()`, take a loot to my answer. It does not matter how big the `join` timeout value is, the important thing is to specify one, this way it keeps responsive. To my mind using a a sleep timer with such short response seems a too hacky workaround way for solving a threading problem. – Konrad Reiche Jun 21 '12 at 23:54
  • 1
    @AlexMartelli this code has an infinite loop as the main thread stays active. This means `threading.active_count()` will always return at least 1. – tvervack Dec 16 '14 at 12:21
  • @FlashYoshi +1. `threading.active_count() > 1: time.sleep(0.1)` – maxkoryukov Sep 16 '16 at 01:01
  • Join has a timeout that can be handy. Also, make your code pythonic by doing `for t in threads:` rather than `for i in range(0, thread_count):` – coder.in.me Sep 27 '16 at 14:19
16

There're two main ways, one clean and one easy.

The clean way is to catch KeyboardInterrupt in your main thread, and set a flag your background threads can check so they know to exit; here's a simple/slightly-messy version using a global:

exitapp = False
if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        exitapp = True
        raise

def threadCode(...):
    while not exitapp:
        # do work here, watch for exitapp to be True

The messy but easy way is to catch KeyboardInterrupt and call os._exit(), which terminates all threads immediately.

Walter Mundt
  • 22,993
  • 5
  • 50
  • 60
5

A Worker might be helpful for you:

#!/usr/bin/env python

import sys, time
from threading import *
from collections import deque

class Worker(object):
    def __init__(self, concurrent=1):
        self.concurrent = concurrent
        self.queue = deque([])
        self.threads = []
        self.keep_interrupt = False

    def _retain_threads(self):
        while len(self.threads) < self.concurrent:
            t = Thread(target=self._run, args=[self])
            t.setDaemon(True)
            t.start()
            self.threads.append(t)


    def _run(self, *args):
        while self.queue and not self.keep_interrupt:
            func, args, kargs = self.queue.popleft()
            func(*args, **kargs)

    def add_task(self, func, *args, **kargs):
        self.queue.append((func, args, kargs))

    def start(self, block=False):
        self._retain_threads()

        if block:
            try:
                while self.threads:
                    self.threads = [t.join(1) or t for t in self.threads if t.isAlive()]
                    if self.queue:
                        self._retain_threads()
            except KeyboardInterrupt:
                self.keep_interrupt = True
                print "alive threads: %d; outstanding tasks: %d" % (len(self.threads), len(self.queue))
                print "terminating..."


# example
print "starting..."
worker = Worker(concurrent=50)

def do_work():
    print "item %d done." % len(items)
    time.sleep(3)

def main():
    for i in xrange(1000):
        worker.add_task(do_work)
    worker.start(True)

main()
print "done."

# to keep shell alive
sys.stdin.readlines()
McGarnagle
  • 96,448
  • 30
  • 213
  • 255
Mark Ma
  • 51
  • 1
  • 1
4

I would rather go with the code proposed in this blog post:

def main(args):

    threads = []
    for i in range(10):
        t = Worker()
        threads.append(t)
        t.start()

    while len(threads) > 0:
        try:
            # Join all threads using a timeout so it doesn't block
            # Filter out threads which have been joined or are None
            threads = [t.join(1000) for t in threads if t is not None and t.isAlive()]
        except KeyboardInterrupt:
            print "Ctrl-c received! Sending kill to threads..."
            for t in threads:
                t.kill_received = True

What I have changed is the t.join from t.join(1) to t.join(1000). The actual number of seconds does not matter, unless you specify a timeout number, the main thread will stay responsive to Ctrl+C. The except on KeyboardInterrupt makes the signal handling more explicit.

Konrad Reiche
  • 25,626
  • 13
  • 100
  • 139
  • 3
    This code will break after the first loop as `t.join(1000)` will not return the thread, but `None`. Hence, after the first loop you will have a list of `None`'s in `threads`. – tmbo May 31 '17 at 08:51
  • This is the best technique for waiting for a thread to join without busy-waiting but still allowing for a SIGINT – FujiApple Jan 23 '18 at 07:23
4

You can always set your threads to "daemon" threads like:

t.daemon = True
t.start()

And whenever the main thread dies all threads will die with it.

http://www.regexprn.com/2010/05/killing-multithreaded-python-programs.html

Hjulle
  • 2,018
  • 1
  • 19
  • 31
Ehsanjs
  • 125
  • 2
  • 7
1

If you spawn a Thread like so - myThread = Thread(target = function) - and then do myThread.start(); myThread.join(). When CTRL-C is initiated, the main thread doesn't exit because it is waiting on that blocking myThread.join() call. To fix this, simply put in a timeout on the .join() call. The timeout can be as long as you wish. If you want it to wait indefinitely, just put in a really long timeout, like 99999. It's also good practice to do myThread.daemon = True so all the threads exit when the main thread(non-daemon) exits.

Milean
  • 798
  • 6
  • 16
1
thread1 = threading.Thread(target=your_procedure, args = (arg_1, arg_2))    
try:
      thread1.setDaemon(True)  # very important
      thread1.start()
except (KeyboardInterrupt, SystemExit):
      cleanup_stop_thread()
      sys.exit()

When you want to kill the thread just use:

thread1.join(0)
evandrix
  • 5,608
  • 4
  • 25
  • 33
Charles P.
  • 1,479
  • 12
  • 13