
Description
Here's a problem that caught my attention after reading @matklad's blog post titled Stopping a Rust worker. While the presented thread stopping mechanism is indeed very nice and convenient, I think a serious flaw is being overlooked: the spawned thread is never joined.
I've also noticed that spawning threads without ever joining has become a common pattern in Rust code. As a good example, take a peek at IndraDB's codebase.
Seeing something like the following should give everyone a pause and make them think twice:
thread::spawn(|| {
// ...
});
Not only this thread is not joined, but the JoinHandle
returned by thread::spawn
is completely ignored. This is a pattern that should be discouraged.
So what's all the fuss about - why is not joining dangerous? Consider this:
use std::sync::mpsc;
use std::thread;
struct Foo;
impl Drop for Foo {
fn drop(&mut self) {
println!("Important cleanup work...");
}
}
fn main() {
let (s, r) = mpsc::channel::<()>();
thread::spawn(move || {
// Initialize some kind of resource.
let _foo = Foo;
// This loop ends when the channel becomes closed.
for msg in r {
// Process the message.
}
});
// Close the channel.
drop(s);
}
At the end of the main function, the channel is closed and the worker thread is signalled that no more messages will be sent. This automatically breaks the loop, which is nice.
However, the thread will not have enough time to actually take notice and drop all its variables. Instead, the thread is abruptly killed. That means it doesn't have a chance to do any kind of final cleanup work, like flushing buffers to disk, closing files, deallocating memory, or anything like that.
Even worse - the exit code is 0, Rust doesn't give any warnings, and everything looks good, while it really isn't!
I believe we should mark JoinHandle
with #[must_use]
. And if the user really doesn't want to join the thread (which should be very rare), they should acknowledge the possibility of the thread silently getting killed and write let _handle = thread::spawn(...)
instead.