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?

1 Like

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

I’ve no idea what the “pythonic” solution to this is. I’m sure there are a million hyperbole. I’ve come to like python, but the existence of this discussion leads me to conclude that python has a weakness here. Other Python threading weaknesses are already known.

To me, the problem seems to be that python does not separate the “signal state” of a generic lock object, from the concept of “ownership”. Actually ownership is just a special case of the signalled state. One still needs to know with atomicity if the object is signalled, even if one doesn’t own it. Presently ownership is the only atomically known state for the lock objects.

  • It needs to be possible to wait on multiple objects.
  • If you’re blocked on the end of a pipe, you can’t be waiting for an exit event as well.

To wait for multiple objects, it is necessary to separate the signalled state from the ownership. This can only be done efficiently with privileged access to the locking system. This would take the form of a method that python provides to wait for multiple objects. Given the existing implementation, it would be hard to add such a function. Lock objects would need a common base class to manage the signalled state. The existing subclasses would still deal with ownership.

It is already possible to create a class that would allow one thread to wait on multiple locks. It is not efficient however. To implement it, the class could create and parent a new thread for each lock registered with the multiple wait class. Each new thread would wait on the lock, and message the parent class if ownership is acquired. The parent class could then signal an event to release threads waiting for any, or all, of the multiple objects.

To have a new thread just to wait on each lock is practical but crazy. The multiple wait has to come from the python framework, because it can interrogate the signalled state of each lock atomically and without a thread for each lock.

If there were a multiple wait available, one would wait on both the pipe and the exit event. Either can release the thread block. When the multiple wait is released, the thread can process the information in the pipe, do a simple exit, or both.

Relevant project for this discussion:

There’s an accompanying talk with even more information. The project is dead, but I still like the idea of this library a lot.