How to share a struct amongst two other structs and also across thread boundary?

So here is the background. I have a struck with some implementation. Let's say

struct Utility {
    state: Vec<String>
} 
impl Utility {
   pub fn new(state: Vec<String>) -> Self {
       Self {
           state
       }
   }
  
 pub fn do_stuff(&self) -> () {
      println!("does stuff");
 }

}

Then I had another struct that needed the above.

struct Task {
   utility: Utility
}

Then I needed to have another task that also needs Utility. So created this

struct Job {
  utility: Utility
}

The obvious problem is, once I create either Job or Task and pass Utility to it, I can't create the other because Utility would have moved.

My first reaction was to make Utility shared. That is define both Task and Job as follows:

struct Job {
  utility: &Utility
}

struct Task {
   utility: &Utility
}

But as soon as I do this, I start getting errors related to missing lifetime specifier. At this stage of learning Rust, I know next to little about lifetimes...

The other thing is that at the end of the day, both Task and Job will actually have to be created and used in separate threads. Meaning they will be structs across thread boundary that will have to make use of Utility. And I have a feeling dealing with shared things across threads has its own solution that might not involve having to fiddle with lifetimes.

So my question is, how do one go about this? Is sorting the lifetime errors the solution? or there are other ways, especially given that sharing across threads will be involved?

You probably want Arc<Utility> or Arc<Mutex<Utility>>, depending on whether or not Job and/or Task needs to mutate Utility.

1 Like

Ok...seems wrapping Utility in Arc ie Arc<Utility> seems to have made the compiler happy...

You are correct. wrapping in Arc seems to have worked. I am just now curious in what situations do one use lifetime annotations and in what situations to things like Arc come in handy?

Normal references (& and &mut) are primarily used for short-term access, like function arguments, because they have a slightly lower runtime overhead than something like Arc.

This brings severe restrictions on how they can be used, however, so storing them longer-term in a lifetime-annotated structure is more of an advanced, niche technique.

2 Likes

For completeness, to make the compiler happy with a reference in a case like this, you would have to write something like:

struct Job<'a> {
  utility: &'a Utility
}

Note the added "lifetime parameter" 'a (where a can be any name, of course), that a caller may now provide. The meaning is that the Utility referenced by this Job will be alive for some lifetime, and it must be at least as long as as the lifetime provided for the type parameter 'a. The compiler will infer a value from usage, or you can provide a lifetime parameter from a function (with much the same syntax: fn name<'a>() {}), but either way different lifetimes will result in different types, the same as a type parameter T on Vec<T> being different makes a different type

The result is that these types are generally tricky to use across multiple functions, as you need to be careful to ensure they all end up with the same lifetime. This is not for the faint hearted, and generally doesn't result in faster code anyway, but sometimes it is useful to implement some nice easy to use API like MutexGuard, or in a library that's going to extra effort trying to avoid requiring allocations where it might matter to some users, such as serde

There's one other option, where the data will live for the entire program (not just as long as even main()!), where you can instead use 'static as the lifetime for the reference. This is mostly for literal strings, eg "foo" has the type &'static str, which can be named and used anywhere, since the string will never be destroyed.

1 Like