David Beazley is the author of Python Essential Reference, 4th Edition and Python Cookbook, 3rd Edition.
When threads sleep, do they dream?
Interaction between async functions and synchronous functions is tricky. Why do we use async at all? Sure, if you're working at a huge scale, it's useful, but most of us don't do that. Some point at the GIL, but that's not exactly solved by using async functions at all.
The best answer is: thread programming. Have you programmed with threads? It's weird and annoying and hard to debug and prone to error. Creating a thread is weird and annoying, getting results from threads is weird and annoying (helloo, futures!), and then think about cancelling or killing a thread … oof. Yeah. This is the point where people say "Well actually, have you considered using ctypes?", and that's a good point to just stop.
But could we rebuild
threads instead? (Fair warning: nothing that's production ready. All of the points below were demonstrated as working).
[I'm skipping the hilarious examples here, because typing up live-coding jokes is worse than explaining them.]
Imagine a new
threado library. How would we want to use it? (Details on some points are below.)
- We'd want a
threado.spawn(func, arg1, arg2)interface to run the function with its arguments, without weird inheritance and magic arguments.
- We'd want to be able to catch exceptions from threads instead of silently crashing without communications.
- We'd want to be sure that we have actual threads, and they won't block each other/the same core/etc.
- We'd want to be able to kill off a thread, with something like a
t.cancel()interface. As a bonus, we'd like to be able to catch and intercept that signal in our thread (
except threado.ThreadCancelled) to deal with it.
- We'd want context manager locking:
- We'd want queueing with some
with threado.ThreadGroup as tmechanism, the regular python queueing mechanisms (
queue.task_done()), and a
- We'd want a
with threado.more_magic()context manager you can use to IMPORT A LIBRARY WHICH MAKES LIBRARY CALLS THREADED AND CANCELLABLE. Sorry, but the capslock was appropriate.
Implications of killing threads
Killing a thread has implications for locking – if a thread is cancelled while using a lock, what do we do? What happens to the lock? You have to give it up somehow, but you also have to take care to do proper cleanup. Luckily, Python context managers are the response – as long as you use locks as a
with lock: context, cancelling the thread will invoke the lock's cleanup.
We can also avoid deadlocks by cancelling threads after a
threado.TimeOut, now that cancelling isn't evil anymore.
Cancellation can only take place on things that block, though – if you place a spinlock on a
ThreadGroup, it won't be cancelable.