Spawn threads and join in destructor

How do i spawn a thread and join it in destructor ? This seems natural to me but is prevented because JoinHandler's join moves the value out. How do I achieve this idiom ?

struct A {
    flag: Arc<Mutex<bool>>,
    join_handle: thread::JoinHandle<()>,
};

impl A {
    pub fn new() -> A {
        let flag = Arc::new(Mutex::new(false));
        let flag_clone = flag.clone();
        A {
            flag: flag,
            join_handle: thread::spawn(move || {
                while !*flag_clone.lock().unwrap() { /* Do Something */ }
            }),
        }
    }
}

impl Drop for A {
    fn drop(&mut self) {
        *self.flag.lock().unwrap() = true;
        self.join_handle.join(); // Error: cannot move out of type `A`, which defines the `Drop` trait
    }
}
1 Like

The usual way to solve this is using what is known as the option dance!

Store an optional value, but it will always be Some until destruction:

join_handle: Option<thread::JoinHandle<()>>,

Since it's an option, we can move it out with mutation by leaving another valid value in its place (None):

self.join_handle.take().unwrap().join()
12 Likes

hahaha .. Brilliant !! Need much more practice with Rust to actually start thinking those myself. Do post these things somewhere and let us all know if there are more nifty/interesting tricks like theses up your sleeve :smile:

Oh I guess we have tons of secret tricks like this by now, I wonder where we should archive them all. I'm reminded of the tricks needed to convince a &mut to move and not of reborrow.

Is the "option dance" still the recommended way to do this? I wonder if, for example, Box is now preferred

It's still the recommended way for cases like this. A Box doesn't help here because you can't move out of it inside drop() for the same reason you cannot call JoinHandle::join() in there - self is borrowed, and you cannot move out of it.

1 Like

Not only that but Option doesn't require a heap allocation and all the cache-unfriendliness resulting from it.

To be fair, that's unlikely to be a concern in this particular case :slight_smile:.

BTW, I'm not sure if @blakehawkins's question is general or specifically about joining a thread in drop(), but if it's the latter, it's worth mentioning that drop() isn't guaranteed to run (e.g. someone can std::mem::forget() the value) and so relying on it for joining a thread is brittle.

To be fair, that’s unlikely to be a concern in this particular case :slight_smile:.

Fair enough :slight_smile:

Even though std::mem::forget is not marked as unsafe, I'm wondering whether it should be precisely because it allows you to skirt the regular ownership rules, similarly to unsafe itself.
My point is that once you use that kind of thing somewhere, all guarantees regarding memory safety are up to the programmer to provide rather than the compiler, and I see whether that is thread-related or not as a more shallow issue.

Leaking memory (and thus foregoing Drop) is safe, on its own. In this particular case, there won't be a memory safety issue - the caller simply won't know the result of the thread's computation, nor whether it finished (since it's running detached). But that's not a "safety" issue in terms of how Rust defines the word.

Back in the day, when JoinHandle itself had a Drop that was relied upon to allow the thread closure to borrow from the environment, now that was a safety issue because the thread can end up accessing invalid memory.

2 Likes

forget was changed from unsafe to safe in large part because it's not feasible to prevent safe Rust code from leaking values. Even without forget you can write safe Rust code that prevents destructors from running, for example by creating a cycle of Rc pointers. There's no way for forget to cause undefined behavior in safe Rust code as long as unsafe code doesn't rely on destructors running. The Rustonomicon has more discussion of this.

3 Likes