Of course there isn’t one! Joining on drop seems to be a better default, but it brings its own problems. The nastiest one is deadlocks: if you are joining a thread which waits for something else, you might wait forever. I don’t think there’s an easy solution here: not joining the thread lets you forget about the deadlock, and may even make it go away (if a child thread is blocked on the parent thread), but you’ll get a detached thread on your hands! The fix is to just arrange the threads in such a way that shutdown is always orderly and clean. Ideally, shutdown should work the same for both the happy and panicking path.

I want to discuss a specific instructive issue that I’ve solved in rust-analyzer. It was about the usual setup with a worker thread that consumes items from a channel, roughly like this:

1 2 3 4 5 6 7 8 9 10 fn frobnicate () { let ( sender , receiver ) = channel (); let worker = jod_thread :: spawn ( move || { for item receiver { do_work ( item ) } }); // prepare some work and send it via sender }

Here, the worker thread has a simple termination condition: it stops when the channel is closed. However, here lies the problem: we create the channel before the thread, so the sender is dropped after the worker . This is a deadlock: frobnicate waits for worker to exit, and worker waits for frobnicate to drop the sender !

There’s a straightforward fix: drop the sender first!

1 2 3 4 5 6 7 8 9 10 11 12 13 fn frobnicate () { let ( sender , receiver ) = channel (); let worker = jod_thread :: spawn ( move || { for item receiver { do_work ( item ) } }); // prepare some work and send it via sender drop ( sender ); drop ( worker ); }

This solution, while obvious, has a pretty serious problem! The prepare some work ... bit of code can contain early returns due to error handling or it may panic. In both case the result is a deadlock. What is the worst, now deadlock happens only on the unhappy path!

There is an elegant, but tricky fix for this. Take a minute to think about it! How to change the above snippet such that the worker thread is guranted to be joined, without deadlocks, regardless of the exit condition (normal termination, ? , panic) of frobnicate ?

The answer will be below these beautiful Ukiyo-e prints :-)

Fine Wind, Clear Morning

Rainstorm Beneath the Summit

First of all, the problem we are seeing here is an instance of a very general setup. We have a bug which only manifests itself if a rare error condition arises. In some sense, we have a bug in the (implicit) error handling (just like 92% of critical bugs). The solutions here are a classic:

Artificially trigger unhappy path often ("restoring from backup every night"). Make sure that there aren’t different happy and unhappy paths ("crash only software").

We are going to do the second one. Specifically, we’ll arrange the code in such way that compiler automatically drops worker first, without the need for explicit drop .

Something like this:

1 2 let worker = jod_thread :: spawn ( move || { ... }); let ( sender , receiver ) = channel ();

The problem here is that we need receiver inside the worker, but moving let (sender, receiver) up brings us back to the square one. Instead, we do this:

1 2 3 let worker ; let ( sender , receiver ) = channel (); worker = jod_thread :: spawn ( move || { ... });

Beautiful, isn’t it? And super cryptic: the real code has a sizable comment chunk!