22

I read somewhere that KeyboardInterrupt exception is only raised in the main thread in Python. I also read that the main thread is blocked while the child thread executes. So, does this mean that CTRL+C can never reach to the child thread. I tried the following code:

def main():
    try:
        thread = threading.Thread(target=f)
        thread.start()  # thread is totally blocking (e.g., while True)
        thread.join()
    except KeyboardInterrupt:
        print "Ctrl+C pressed..."
        sys.exit(1)

def f():
    while True:
        pass  # do the actual work

In this case there is no effect of CTRL+C on the execution. It is like it is not able to listen to the signal. Am I understanding this the wrong way? Is there any other way to kill the thread using CTRL+C?

Maggyero
  • 3,120
  • 2
  • 24
  • 43
Amit S
  • 1,033
  • 2
  • 9
  • 18
  • See http://stackoverflow.com/questions/323972/is-there-any-way-to-kill-a-thread-in-python – Rafe Kettler Nov 09 '10 at 17:33
  • 6
    Your main thread isn't blocked because you start another thread. What would be the point of threads if that were true? It's because you're calling thread1.join(), which DOES block until thread1 is done. – Falmarri Nov 09 '10 at 17:42

3 Answers3

13

If you want to have main thread to receive the CTRL+C signal while joining, it can be done by adding timeout to join() call.

The following seems to be working (don't forget to add daemon=True if you want main to actually end):

thread1.start()
while True:
    thread1.join(600)
    if not thread1.isAlive():
        break
Mwiza
  • 4,494
  • 2
  • 33
  • 30
korc
  • 239
  • 2
  • 2
10

The problem there is that you are using thread1.join(), which will cause your program to wait until that thread finishes to continue.

The signals will always be caught by the main process, because it's the one that receives the signals, it's the process that has threads.

Doing it as you show, you are basically running a 'normal' application, without thread features, as you start 1 thread and wait until it finishes to continue.

Junuxx
  • 13,097
  • 5
  • 36
  • 67
webbi
  • 789
  • 6
  • 14
  • But the main thread still receives exit signal right? so why doesn't it send the signal to its child and both exits? – y_159 Apr 10 '21 at 11:07
4

In Python, it is true that KeyboardInterrupt exceptions are raised only in the main thread of each process. But as other answers mentionned, it is also true that the method Thread.join blocks the calling thread, including KeyboardInterrupt exceptions. That is why Ctrl+C seems to have no effect: the execution in the main thread remains blocked at the line thread.join().

So a simple solution to your question is to firstly, add a timeout argument to thread.join() and put that call in a loop that ends when the child thread exits, so that KeyboardInterrupt exceptions can be raised after each timeout, and secondly, make the child thread daemonic, which means that its parent (the main thread here) will kill it when it exits (only non-daemon threads are not killed but joined when their parent exits):

def main():
    try:
        thread = threading.Thread(target=f)
        thread.daemon = True  # create a daemon child thread
        thread.start()

        while thread.is_alive():
            thread.join(1)  # join shortly to not block KeyboardInterrupt
    except KeyboardInterrupt:
        print "Ctrl+C pressed..."
        sys.exit(1)

def f():
    while True:
        pass  # do the actual work

But a better solution, if you control the child thread's code, is to inform the child thread to exit gracefully (instead of abruptly like with the first solution), for instance with a threading.Event:

def main():
    try:
        event = threading.Event()
        thread = threading.Thread(target=f, args=(event,))
        thread.start()
        event.wait()  # wait forever but without blocking KeyboardInterrupt
    except KeyboardInterrupt:
        print "Ctrl+C pressed..."
        event.set()  # inform the child thread that it should exit
        sys.exit(1)

def f(event):
    while not event.is_set():
        pass  # do the actual work
Maggyero
  • 3,120
  • 2
  • 24
  • 43
  • This doesn't seem to work for me: using Python 3.8.x. A `KeyboardInterrupt` doesn't seem to be catchable as far as I can work out. In other code I'm getting a thing saying "exception was ignored" when a keyboard interrupt occurs. – mike rodent Apr 05 '21 at 06:47
  • @mikerodent Have you tried the two examples in this answer? – Maggyero Apr 05 '21 at 08:27
  • Going to delete my comment... am slowly learning how these things work. My solution to graceful shutdown and responsiveness to Ctrl-C (https://stackoverflow.com/a/66949660/595305): from my experiments it seems that a `while True:` loop on the main thread is necessary... – mike rodent Apr 05 '21 at 08:32
  • @Maggyero is this what a signalling is? – y_159 Apr 10 '21 at 11:16