Joining a thread in a method that takes `&mut self` (like drop) results in "cannot move out of borrowed content"

The function signature of JoinHandle::join is:

fn join(self) -> Result<T>

This means that the method takes self (the receiver object) by values (taking the ownership/consuming it). But you only have a borrow to your JoinHandle; a mutable one, but still merely a borrow, not the ownership. Thus you can't call this method, because you can't move the ownership out of your borrow into this join() method.

An easy way to fix that, is by accepting self by value in the stop() method, too:

pub fn stop(self) {
    self.handle.join();
}

But you will notice that this isn't possible when implementing Drop, because drop() has the signature fn drop(&mut self)! Bummer! But there is a little trick you can use, described below. Please be aware that joining threads in drop() is probably not a good idea! Read Matthieu M.'s answer for more information on that!

If you still think, for whatever reason, that you really want to join a thread in drop(), you can store the JoinHandle in an Option<T> to save whether or not it's already joined. If you have a Some(T) you can obtain a T (by value!) from it by using the method Option::take(). Then you can write:

fn drop(&mut self) {
    // `self.handle` has the type `Option<JoinHandle<()>>` here!
    if let Some(handle) = self.handle.take() {
        handle.join().expect("failed to join thread");
    }
}

Don't.


It may seem counter-intuitive, but joining a thread (or process) in a destructor is generally a bad idea.

Note that asking to join does not cause the thread to stop by itself; it's just about waiting for the thread to stop, and the thread may not. There are multiple reasons why this could happen:

  • the handle being on the same thread as the one it controls,
  • the thread to be stopped waiting on a channel the current thread should send a signal on,
  • ...

Yes, that's a deadlock. An implicit deadlock.

A particular nasty situation is if your current thread panics (something unforeseen occurred). Unwinding starts... and blocks! And all the resources that this thread was supposed to clean-up hung in limbo, forever.


A better design is to instead create an explicit join method, which consume self (by value). It also lets you return a Result, in case joining causes an error.

And in order for your users to remember to join explicitly, panic! in the Drop implementation if they forgot to.

That is:

impl Foo {
    fn join(mut self) -> std::thread::Result<()> {
        match self.handle.take() {
            Some(h) => h.join(),
            None => Ok(()),
        }
    }
}

impl Drop for Foo {
    fn drop(&mut self) {
        if self.handle.is_some() {
            panic!("You MUST call either join on `Foo` to clean it up.");
        }
    }
}

Note: I am aware that panicking in destructors is controversial, however it is much safer to abort a process when it's in an unknown state that go on and hope for the best.


If you really, despite my warning, want to shoot yourself in the foot, join in drop.


The problem in join signature:

fn join(self) -> Result<T>

so to fix your code, you need something like:

pub fn stop(self) {
    self.handle.join();
}