Daemon threads and background task termination

Context managers solve the application use case, but they don’t solve the interactive use case.

All the potential cleanup triggers in interactive use don’t actually work:

  • context managers: with statements can only apply to a single interactive command, they can’t span multiple commands
  • contextlib.ExitStack: still needs a with statement or some other callback to trigger cleanup
  • __del__: the __main__ module globals are only cleared after threads are joined at shutdown
  • atexit: these hooks also run after threads are joined at shutdown

The last two can be made to work, but only if you mark the background thread as a daemon thread so it gets ignored by the “wait for all non-daemon threads to terminate” step at shutdown, and then set up an appropriate atexit hook to trigger the lazy cleanup.

This means the current two simplest ways to implement background threads for synchronous applications are to:

  1. Just make them regular threads, with only deterministic cleanup supported. These APIs hang on shutdown if you attempt to use them interactively.
  2. Make the background threads daemon threads, without arranging to clean them up before shutdown (in the absence of deterministic cleanup). These APIs are likely to throw exceptions on shutdown if you attempt to use them interactively.

I do think public thread-safe APIs to schedule tasks and run arbitrary callables in the background thread’s event loop would be worthwhile additions (my actual implementation has them), but the sample code in the post was already complicated enough without them (one subtle point with such injections is that it’s OK for them to be outside the termination task group, since the loop shutdown will terminate everything else after the main task gets terminated).

(Given the impact on the threading API and the shutdown process, I think this idea would need a PEP to be actually implemented, but I wanted to get feedback on it before investing that kind of time into it)

2 Likes