Hashmap lifetime trouble when value passed to thread

Hi folks, I am perplexed. I create a hashmap where the values are traits. I get a trait from it, then spawn a thread to run the function. The compiler complains that my hashmap does not live long enough and is dropped...at the very last line in the program! I'm not sure how I can make it live longer! Help please? And thanks in advance!

use std::sync::{Arc, Mutex};
use std::thread;
use std::collections::HashMap;
fn main() {
  let mut my_trait: Arc<Mutex<Box<dyn MyTrait + Send>>> = Arc::new(Mutex::new(Box::new(MyStruct{num: 41})));
  let mut hm : HashMap<u64, Arc<Mutex<Box<dyn MyTrait + Send>>>> = HashMap::new();
  hm.insert(0, my_trait);
  match hm.get(&mut(0 as u64)) {
    Some( mt) => {
      let j: thread::JoinHandle<_> = thread::spawn(move || {
        mt.lock().unwrap().my_trait_fun()
      });
      j.join().expect("panic");
    }
    None => { println!("hashmap error"); }
  }

  println!("done")
}

pub trait MyTrait {
  fn my_trait_fun(& mut self);
}

pub struct MyStruct {
  num: u32,
}

impl MyTrait for MyStruct {
  fn my_trait_fun(& mut self) {
    self.num += 1;
    println!("num: {}", self.num);
  }
}

You correctly put the thing being shared in an Arc, but HashMap::get returns a regular borrowed reference to the value, so inside the match, mt is a &Arc<Mutex<...>>. You need to clone it to get an owned Arc that can be moved into std::thread.

Also note that Arc<Mutex<Box<...>>> is fairly uncommon; you usually don't need the inner pointer. Arc<Mutex<dyn MyTrait + Send>> works fine here.

    let my_trait: Arc<Mutex<dyn MyTrait + Send>> = Arc::new(Mutex::new(MyStruct { num: 41 }));
    let mut hm: HashMap<u64, _> = HashMap::new();
    hm.insert(0, my_trait);
    match hm.get(&0) {
        Some(mt) => {
            let mt = Arc::clone(mt);  // here's one way to do it
            let j: thread::JoinHandle<_> = thread::spawn(move || mt.lock().unwrap().my_trait_fun());
            j.join().expect("panic");
        }
        None => {
            println!("hashmap error");
        }
    }
2 Likes

This video by Ryan Levick walks you through a situation which is at least similar to what you're struggling with here, if I remember it correctly. I tried playing with your code and didn't manage to get it to accept hm. I believe the problem is that main ends while the thread holding hm's element could still be alive. hm would be destroyed at the end of main even though mt is moved into the thread. I think Ryan starts discussing this kind of thing at about 1:01:05 of the video. It's a video I've planned to re-watch as I've also struggled with this in the past.

Thanks Trentj, I learned something today!

I'm curious though why you wouldn't typically put a Box value into a hashmap. In my case, the trait object data (i.e. the hashmap value) needs to live on the heap, because ultimately (unlike this simplified use case) the values to be added to the hashmap will be passed in from a function. If the values are stack variables in the calling function, they go out of scope when the function returns.

My downfall was trying to wrap hm in an Arc which led me down a rabbit hole. Yours is a much better way of understanding and solving it.

Hi Bobahop, appreciate your response. I too was concerned about the thread outliving main, which is why I did the JoinHandle thing. I'll definitely watch the video though, thanks for that!

You don't need the Box in this case because the Mutex is already in an Arc, which puts it on the heap. That's how Arc manages to be shared and 'static at the same time.

Arc::new moves its argument onto the heap. Yes, an Arc may go out of scope when the function returns, but that just reduces the reference count; the actual Mutex<MyStruct> inside it, even if it was briefly stored on the stack, won't be dropped until all the Arcs that refer to it have been dropped. That includes Arcs that have been cloned and moved to other threads.

1 Like

So much going on behind the scenes! Appreciate the succinct explanation. You should write a book. You explained in one short paragraph what I couldn't get from reading whole chapters and watching videos. " Arc::new moves its argument onto the heap". Why couldn't they just say that! :slight_smile:

The std library docs do point this out, at least...

A thread-safe reference-counting pointer. 'Arc' stands for 'Atomically Reference Counted'.

The type Arc<T> provides shared ownership of a value of type T , allocated in the heap.

Ah, so the way I interpreted that was that T needed to be allocated on the heap. Not that Arc would do the allocation. Which is why, in my initial implementation, T was Box::new(T). But now I know.

That seems like a totally reasonable interpretation... sprucing up the documentation such that it clearly states Arc provides and manages the heap-allocated storage seems worthwhile.

If you spend some time thinking about the function of Arc, it may become clear that it could only work by imposing heap allocation (or, perhaps, some sort of clever scoped allocation... if such a thing is practical), but stating it explicitly seems a lot more helpful.

1 Like