1

I've seen several threads on here about killing python threads in a clean way, but I think I'm having a more fundamental issue. Suppose I have some code that looks like this:

t1 = threading.Thread(target=method1, args=(args1,))
t1.daemon = True

t2 = threading.Thread(target=method2, args=(args2, ))
t2.daemon = True
t1.start()
t2.start()

while True:
    time.sleep(1)

I would like for the main thread to notice a Ctrl-C keyboard interrupt, and from there I can handle the termination of the other threads (t1 and t2) as appropriate given the context. But no matter what I do, I can't get the main thread to catch the KeyboardInterrupt. I've tried something like this:

try:
    while True: time.sleep(100)
except KeyboardInterrupt:
    print "Quitting!"

Or something like this:

threads = []
threads.append(t1)
threads.append(t2)
while len(threads) > 0:
    try:
        threads = [t for t in threads if t is not None and t.isAlive()]
        time.sleep(1)
    except:
        print "Ctrl - C received, kiling"
        for t in threads:
            t.kill_received = True

But none of these even print the messages in the exception handler, displaying only this:

    Exception in thread Thread-3:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "/usr/local/lib/python2.7/dist-packages/audioread/gstdec.py", line 149, in run
    self.loop.run()
  File "/usr/lib/python2.7/dist-packages/gi/overrides/GLib.py", line 576, in run
    raise KeyboardInterrupt
KeyboardInterrupt

The main question here is not how to kill t1 and t2 safely (I've got to deal with that too, eventually), but why is the main thread not catching KeyboardInterrupt here?

Edit: As I've written the examples, I've already tried the approach involving sleeping in a while loop within the try-except block. Any other ideas?

kaajaa328
  • 51
  • 4
  • This is very mysterious, I tried subclassing `threading.Thread` and copied code from the CPython source for the `run()` method, which has a `try/finally` in it. I added an `except Exception:` to that with a `print` in it, but it's never triggered when a `KeyboardInterrupt` occurs. There must be some deeper magic going on. I think it may have something to do with the fact the the thread are daemonic. – martineau Aug 04 '17 at 18:12
  • For what it's worth, I've also tried it with the daemon attributes set to False.. doesn't help. – kaajaa328 Aug 04 '17 at 18:30
  • Read about [is-there-any-way-to-kill-a-thread-](https://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python), [keyboardinterrupt-in-multithreading](https://stackoverflow.com/questions/43721150/python-keyboardinterrupt-in-multithreading), [keyboardinterrupt](https://stackoverflow.com/questions/4136632/ctrl-c-i-e-keyboardinterrupt-to-kill-threads-in-python) – stovfl Aug 05 '17 at 13:32
  • Possible duplicate of [Python threading ignores KeyboardInterrupt exception](https://stackoverflow.com/questions/3788208/python-threading-ignores-keyboardinterrupt-exception) – stovfl Aug 05 '17 at 13:32
  • I don't think the KeyboardInterrupt is guaranteed to go to the main thread even under normal conditions, and you appear to be using `pygobject`, which installs [its own `SIGINT` handler](https://github.com/GNOME/pygobject/blob/master/gi/overrides/GLib.py#L563) that overrides the default `KeyboardInterrupt` behavior. I suspect the way you're using threads isn't really something `pygobject` supports. – user2357112 Aug 10 '17 at 19:45
  • @martineau: `except Exception:` can't catch `KeyboardInterrupt`, which inherits directly from `BaseException` (because usually, even "generalized" exception handling shouldn't be handling `KeyboardInterrupt`). Catch it explicitly, or use a bare `except:` (equivalent to `except BaseException:`). – ShadowRanger Aug 10 '17 at 20:19
  • @ShadowRanger: Not that simple. I did what you said in my `threading.Thread` subclass and the results stayed the same. Note that both originally and with `except BaseException:` instead of `except Exception:`, a `KeyboardInterrupt` does stop the script, but the clause following (either version of) the `except` doesn't get executed. – martineau Aug 10 '17 at 20:59
  • Read also [here](https://github.com/beetbox/audioread/issues/63), which came up in audioread. There is another possible workaround. – Albert Feb 22 '18 at 17:17

1 Answers1

2

The following works. Even though the try...except in the Thread subclass is never triggered, the main thread is able to catch KeyboardInterrupts.

The code in the run() method of the MyThread subclass is a modified version of what is the CPython 2.7 source for the Thread.run() method, which already contained a try/finally...to which I added an except BaseException as exc: in order to print a message when any type of one happens.

import threading
import time

def method1(args):
    print('method1() running')
    while True:
        time.sleep(.001)

def method2(args):
    print('method2() running')
    while True:
        time.sleep(.01)


class MyThread(threading.Thread):
    def __init__(self, *args, **kwargs):
        super(MyThread, self).__init__(*args, **kwargs)

    def run(self):
        """ Method representing the thread's activity.

        You may override this method in a subclass. The standard run() method
        invokes the callable object passed to the object's constructor as the
        target argument, if any, with sequential and keyword arguments taken
        from the args and kwargs arguments, respectively.
        """
        try:
            print('in MyThread.run()\n')
            if self._Thread__target:
                self._Thread__target(*self._Thread__args, **self._Thread__kwargs)
        except BaseException as exc:
            print('reraising Exception {}'.format(exc))
            raise typeof(exc)(str(exc))
        finally:
            # Avoid a refcycle if the thread is running a function with
            # an argument that has a member that points to the thread.
            del self._Thread__target, self._Thread__args, self._Thread__kwargs
            print('exiting')

args1 = 1
t1 = MyThread(target=method1, args=(args1,))
t1.daemon = True

args2 = 2
t2 = MyThread(target=method2, args=(args2,))
t2.daemon = True
t1.start()
t2.start()

try:
    while True:
        time.sleep(1)
except BaseException as exc:
    print('exception "{}" occurred'.format(type(exc)))
    quit()

Console output (I typed Ctrl-C right after the method1() running appeared):

> python not-catching-keyboardinterrupt.py
in MyThread.run()
in MyThread.run()

method2() running

method1() running
exception "<type 'exceptions.KeyboardInterrupt'>" occurred
martineau
  • 119,623
  • 25
  • 170
  • 301