Emilio
Emilio

Reputation: 4031

threading ignores KeyboardInterrupt exception

I'm running this simple code:

import threading, time

class reqthread(threading.Thread):    
    def run(self):
        for i in range(0, 10):
            time.sleep(1)
            print('.')

try:
    thread = reqthread()
    thread.start()
except (KeyboardInterrupt, SystemExit):
    print('\n! Received keyboard interrupt, quitting threads.\n')

But when I run it, it prints

$ python prova.py
.
.
^C.
.
.
.
.
.
.
.
Exception KeyboardInterrupt in <module 'threading' from '/usr/lib/python2.6/threading.pyc'> ignored

In fact python thread ignore my Ctrl+C keyboard interrupt and doesn't print Received Keyboard Interrupt. Why? What is wrong with this code?

Upvotes: 70

Views: 84594

Answers (6)

Claudio_G
Claudio_G

Reputation: 61

I know this is quite old but I had this exact issue and required the Ctrl-C behavior to work on Docker (ubuntu 20.04) and on Windows. On windows specifically, the signal handling is done only onto the main thread, only when the thread is not in a wait state. This is both true for a try: except KeyboardInterrupt: and for a signal.signal(signal.SIGINT, handler) where either gets raised or called only when the main thread is out of a wait.

For instance if you change your code to the following and press Ctrl-C midway, you will see that the exception gets caught but only when reqThread actually terminates and therefore thread.join() returns.

import threading, time

class reqthread(threading.Thread):
    def run(self):
        for i in range(0, 10):
            time.sleep(1)
            print('.')

try:
    thread = reqthread()
    thread.start()
    thread.join()
except (KeyboardInterrupt, SystemExit):
    print('\n! Received keyboard interrupt, quitting threads.\n')

However, an interesting thing is that when the main thread is running an asyncio loop, it will always catch a Ctrl-C on both Windows and Linux (at least on the docker Ubuntu image I am running).

the following piece of code demonstrates the behavior

import threading, time, signal, asyncio

localLoop = asyncio.new_event_loop()
syncShutdownEvent = threading.Event()

class reqthread(threading.Thread):
    def run(self):
        for i in range(0, 10):
            time.sleep(1)
            print('.')
            if syncShutdownEvent.is_set():
                break

        print("reqthread stopped")

        done()

        return

def done():
    localLoop.call_soon_threadsafe(lambda: localLoop.stop())

def handler(signum, frame):
    signal.getsignal(signum)
    print(f'\n! Received signal {signal.Signals(signum).name}, quitting threads.\n')
    syncShutdownEvent.set()

def hookKeyboardInterruptSignals():
    for selectSignal in [x for x in signal.valid_signals() if isinstance(x, signal.Signals) and x.name in ('SIGINT', 'SIGBREAK')]:
        signal.signal(selectSignal.value, handler)

hookKeyboardInterruptSignals()
thread = reqthread()
thread.start()
asyncio.set_event_loop(localLoop)
localLoop.run_forever()
localLoop.close()

and will give you the same behavior on both Windows and Ubuntu

python scratch_14.py
.
.

! Received keyboard interrupt, quitting threads.

.
reqthread stopped

for my specific application where is need to use 1 thread running synchronous code and 1 thread running async code i actually use a total of three threads.

  1. Main thread, running the Ctrl-C asyncio catcher
  2. Synchronous thread
  3. Asyncio loop thread

EDIT: fixed a typo that caused the first code block import statement to be interpreted as plain text instead of part of the code block

Upvotes: 0

yaccob
yaccob

Reputation: 1353

Slight modification of Ubuntu's solution.

Removing tread.daemon = True as suggested by Eric and replacing the sleeping loop by signal.pause():

import signal

try:
  thread = reqthread()
  thread.start()
  signal.pause()  # instead of: while True: time.sleep(100)
except (KeyboardInterrupt, SystemExit):
  print('Received keyboard interrupt, quitting threads.)

Upvotes: 9

unutbu
unutbu

Reputation: 880797

Try

try:
    thread = reqthread()
    thread.daemon = True
    thread.start()
    while True:
        time.sleep(100)
except (KeyboardInterrupt, SystemExit):
    print('Received keyboard interrupt, quitting threads.')
 

Without the call to time.sleep, the main process is jumping out of the try...except block too early, so the KeyboardInterrupt is not caught. My first thought was to use thread.join, but that seems to block the main process (ignoring KeyboardInterrupt) until the thread is finished.

thread.daemon=True causes the thread to terminate when the main process ends.

Upvotes: 79

personal_cloud
personal_cloud

Reputation: 4524

Putting the try ... except in each thread and also a signal.pause() in true main() works for me.

Watch out for import lock though. I am guessing this is why Python doesn't solve ctrl-C by default.

Upvotes: 0

rattray
rattray

Reputation: 6089

To summarize the changes recommended in the comments, the following works well for me:

try:
  thread = reqthread()
  thread.start()
  while thread.isAlive(): 
    thread.join(1)  # not sure if there is an appreciable cost to this.
except (KeyboardInterrupt, SystemExit):
  print '\n! Received keyboard interrupt, quitting threads.\n'
  sys.exit()

Upvotes: 17

Albert
Albert

Reputation: 68330

My (hacky) solution is to monkey-patch Thread.join() like this:

def initThreadJoinHack():
  import threading, thread
  mainThread = threading.currentThread()
  assert isinstance(mainThread, threading._MainThread)
  mainThreadId = thread.get_ident()
  join_orig = threading.Thread.join
  def join_hacked(threadObj, timeout=None):
    """
    :type threadObj: threading.Thread
    :type timeout: float|None
    """
    if timeout is None and thread.get_ident() == mainThreadId:
      # This is a HACK for Thread.join() if we are in the main thread.
      # In that case, a Thread.join(timeout=None) would hang and even not respond to signals
      # because signals will get delivered to other threads and Python would forward
      # them for delayed handling to the main thread which hangs.
      # See CPython signalmodule.c.
      # Currently the best solution I can think of:
      while threadObj.isAlive():
        join_orig(threadObj, timeout=0.1)
    else:
      # In all other cases, we can use the original.
      join_orig(threadObj, timeout=timeout)
  threading.Thread.join = join_hacked

Upvotes: 0

Related Questions