Making it simpler to gracefully exit threads

Currently, telling a thread to gracefully exit requires setting up some custom signaling mechanism, for example via a threading.Event. To make this process a bit more efficient, cleaner and simpler, what if some methods were added to:

  1. Set a soft interruption flag for a specific thread, such as threading.Thread.interrupt.
  2. Set the interruption flag for all threads, such as threading.interrupt_all_threads.
  3. Query whether the current thread’s interruption flag is set, such as threading.interruption_requested.

An example snippet demonstrating the use of these methods:

from time import sleep
from threading import Thread, interruption_requested, interrupt_all_threads

def loop():
  while not interruption_requested():
    sleep(1)
  print('exiting thread!')

threads = [Thread(target=loop) for i in range(8)]
for thread in threads:
  thread.start()

try:
  # tell the first four threads to stop after five seconds
  sleep(5)
  for thread in threads[:4]:
    thread.interrupt()
    thread.join()
  
  # tell the next four threads to stop after another five seconds
  sleep(5)
  for thread in threads[4:]:
    thread.interrupt()
    thread.join()

except KeyboardInterrupt:
  # tell them all to stop if they haven't already

  interrupt_all_threads()
  for thread in threads:
     thread.join()

Additionally, for convenience, situations such as unhandled SIGHUP, SIGINT/uncaught KeyboardInterrupt or a call to sys.exit in the main thread could automatically set all threads’ interruption flags.

1 Like

You basically want built-in support for thread cancellation in Python? Something similar, on the Python-level, as pthread_cancel?

Doesn’t ThreadPoolExecutor already achieves that?

1 Like

Yes, there is concurrent.futures.Future.cancel().

I don’t know all the details of pthread_cancel, so can’t say for sure how much this idea lines up with that.

The main goal of this suggestion is to simplify a common pattern for graceful thread termination. There are multiple questions on stackoverflow regarding this topic. For example:

Most of these answers are a variation of using a threading.Event or some other shared state to tell the thread that it should exit.

Could you clarify which aspect of ThreadPoolExecutor achieves this and how?

My understanding is that concurrent.futures.Future.cancel can only stop calls that haven’t yet been executed, and there is no way to query the state of the future from within the call itself to tell whether it has been cancelled. Is that not the case?

Yeah - I don’t know if there is a way to see if the Future was cancelled from within that Future - but that’s a bit of a separate issue, is it not (if you can cancel it conveniently, do you care how)? The ‘cancel’ doc is also a bit ambiguous, but suggests that an executing thread could in principle be cancelled – but I’ve never used that API so don’t know. Anyway - I wonder what the core devs think of your request.

IIUC, what you are suggesting is simply a standardized place and access methods for the recommended practice of a termination flag.

+1 on this.

A possible extension: a check_interrupt() function that polls this flag, and if set raises ThreadExit exception, a subclass of SystemExit. This will let any finally: blocks execute on the way out and possibly caught and cancelled, if you choose to.

Another possible extension: Thread.enable_async_interrupt = True indicates that this thread is not going to poll the interruption flag, but it is ok for interruption to raise ThreadExit exception in the thread.

There is a python C API for this (PyThreadState_SetAsyncExc). It is not exposed to python code by default against “naive misuse”. I suggest that applying it only to threads that opt-in should be sufficient.

Finally, another flag, with the proposed name enable_interrupt_io would enable sending a signal to the thread using signal.pthread_kill() to interrupt any blocking I/O that could otherwise prevent the thread from stopping (+windows equivalent, if possible).

These would all keep the same interface to request interruption of a thread, but opt-in to progressively more intrusive implementations.

4 Likes

Something to note is this approach handles stopping active threads, such as an infinite loop periodically doing some work. Another aspect, the ‘graceful’ part, is about stopping in a way that’s less likely to leave things in an untidy or unexpected state. If the thread can check when it’s time to stop, it gets the chance to complete an ongoing task, perform any deinitializations, write out logs, and so on.

I think that’s a good description. A clear and concise summary.

I like this idea; a simple way to incorporate a flag check with other exception handling. Minor nitpick: the name ThreadExit feels a bit mismatched with interrupt(). Maybe something like ThreadInterruptionError? On the other hand, there already is a built-in InterruptedError used for something else so that could be confusing or a source of bugs.

One potential addition to these ideas: a way to explicitly mark which parts of code are asynchronously interruptible. This would make it easier to avoid/defer interruptions at undesired points, such as stopping without releasing an acquired mutex/semaphore, or without completing an important file write. Maybe this could be done through a context manager. For example:

from threading import allow_interruption, Thread, ThreadExit

def loop():
  # do i/o calls to set up i.e. read some settings file and write out some logs based on them, without being interrupted
  # prepare a socket without being interrupted

  while True:
    try:
      with allow_interruption:
        # do blocking wait for socket connection
        # do blocking call to receive data from new connection
    except ThreadExit:
        # close connection
        # break out of loop
    else:
        # do uninterruptible blocking call to send back response
        # close connection

  # write out logs without being interrupted

thread = Thread(target=loop)
thread.enable_async_interrupt = True
thread.enable_io_interrupt = True
thread.start()

# then later on:
thread.interrupt() # raise a ThreadExit next time code inside an allow_interruption context is being run
thread.join()

I would not consider that a “possible” addition but really a “required” addition. Unless the thread has been explicitly marked as safely interruptible at any moment (as in the extension suggested by @orent) I think you need to be able to indicate “safe” points inside a thread where it can/must check if it can safely exit (however that’s implemented). With that I quite like the overall proposal.

Historically, common practice is to mark the regions that should not be interrupted (aka critical sections) rather than the other way around. This is very similar to a lock and would naturally be implemented using with

Specific points where interruption is checked are covered by the proposed check_interrupt()

2 Likes