3

I've been working on my first Rust project recently but have hit a snag. I am using a HashMap mapping Strings to AtomicUsize integers. The HashMap is protected by a RwLock to allow for concurrent access. I would like to be able to return references to AtomicUsize values in the HashMap, however if I try to return these references to the caller past the lifetime of the RwLockWriteGuard I get an error that borrowed value does not live long enough. I've reproduced a minimal example below and put the same example on the Rust playground here.

use std::collections::HashMap;
use std::sync::RwLock;
use std::sync::atomic::{AtomicUsize, Ordering};

struct Bar {
    val: AtomicUsize
}

impl Bar {
    pub fn new() -> Self {
        Bar { val: AtomicUsize::new(0) }
    }
}

struct Foo {
    map: RwLock<HashMap<String, Bar>>
}


impl Foo {
    pub fn get(&self, key: String) -> &Bar {
        self.map.write().unwrap().entry(key).or_insert(Bar::new())
    }
}

fn main() {
    let foo = Foo {map: RwLock::new(HashMap::new())};
    let bar = foo.get("key".to_string());
}

The error I get occurs on the line:

self.map.write().unwrap().entry(key).or_insert(Bar::new())

And is because the borrowed value does not live long enough. I've read a few other posts that discuss this error, this one in particular was especially relevant. After reading it over, I can gather that a value returned from a mutex must have a lifetime less than that of the mutex, which would seem to rule out completely what I'm trying to do. I can see why this should be impossible, because if we have a pointer into the Hashmap and another inserts values into the mutex which cause it to be resized, then we will have a dangling pointer.

My question, then, is twofold. Firstly, I'm just curious if I understand the problem correctly or if there is another reason why I'm disallowed from doing what I tried to do? And my second question is if there is perhaps another way to accomplish what I am trying to do without Box the atomic integers and storing those in the HashMap? Such an approach seems like it should work to me because we can return a pointer to the Boxed value which would always be valid. However it seems like this approach would be inefficient because it would require an extra layer of pointer indirection and an extra allocation. Thanks!

Community
  • 1
  • 1
jeromefroe
  • 1,345
  • 12
  • 19

1 Answers1

8

You're correct that you can't return a reference to something which outlives the MutexGuard, because that would lead to a possibly dangling pointer.

Wrapping the contents in a Box won't help, though! A Box is an owned pointer and apart from the redirection behaves like the contained value as far as reference lifetime goes. After all, if you returned a reference to it, someone else might remove it from the HashMap and de-allocate it.

Depending on what you want to do with the reference, I can think of a couple of options:

  1. Instead of Boxing the values, wrap them in Arc. You would clone the Arc when taking from the HashMap, and multiple references can live at the same time.

  2. You could also return the MutexGuard along with the reference; see this question, which would work well if you just want to operate on the value and then drop the reference relatively soon. This would keep the mutex held until you're finished with it.

Chris Emerson
  • 13,041
  • 3
  • 44
  • 66
  • Thanks for the feedback, this is extremely helpful! I think I will take the approach of using an `Arc` to start because I want to be able to create multiple references to the value that can live well past the `MutexGuard`. For my use case I don't need to delete any values in the `HashMap` once they are inserted so I wonder if an `Arc` might be more heavyweight than I need. I wonder if there is a way to store pointers to the values in the `HashMap` and then use lifetimes to tell the compiler that the reference is valid as long as the `HashMap` is in scope. More digging for me it seems! – jeromefroe Oct 28 '16 at 16:56
  • I worked on this a little more this morning, and came up with a [different solution](https://play.rust-lang.org/?gist=a1d8df52c3a66f7b6a89a6b448a0a89e&version=nightly&backtrace=0) that uses `Shared` pointers instead of `Arc`. It requires `unsafe` code and nightly Rust but I think it's pretty interesting nonetheless. I haven't run any benchmarks yet but I think it should be a little faster. – jeromefroe Oct 29 '16 at 14:46