1

I have a struct which contains a Vec of instances of another base class of struct. I am trying to iterate over the Vec and spawn threads which each run a single impl fn from the base struct. Nothing needs mutable access at any time after the iteration of thread spawning begins; just some basic math returning an f64 (based on values in a HashMap using keys stored in a fixed Vec in each base struct).

I am running into lifetime issues which I don't fully understand and which the compiler error messages (for once) don't help with.

Here is a stripped down version of what I want to implement (with some annotation of the errors encountered):

struct BaseStruct {
    non_copy_field: Vec<&'static str>,  // BaseStruct has vector members (thus can't implement Copy).
}

impl BaseStruct {
    fn target_function(&self, value_dict: &HashMap<&'static str, f64>) -> f64 {
        // Does some calculations, returns the result.
        // Uses self.non_copy_field to get values from value_dict.
        0.0
    }
}

struct StructSet {
    values: HashMap<&'static str, f64>,  // This will be set prior to passing to base_struct.target_function().
    all_structs: Vec<BaseStruct>,        // Vector to be iterated over.
}

impl StructSet {
    fn parallel_calculation(&self) -> f64 {
        let mut result = 0.0;

        let handles: Vec<_> = self.all_structs.iter().map(|base_struct| {
            // Complains about lifetime here   ^^^^^ or    ^ here if I switch to &base_struct
            thread::spawn(move || {
                base_struct.target_function(&self.values)
            })
        }).collect();

        for process in handles.iter() {
            result += process.join().unwrap();
        };
        // Shouldn't all base_structs from self.all_structs.iter() be processed by this point?
        result
    } // Why does it say "...so that reference does not outlive borrowed content" here?
}

I have been trying various combinations of RwLock/Arc/Mutex wrapping the contents of the fields of StructSet to attempt to gain thread-safe, read-only access to each of the elements iterated/passed, but nothing seems to work. I'm looking to keep the codebase light, but I guess I'd consider rayon or similar as I'll need to follow this same process multiple places in the full module.

Can anyone point me in the correct direction?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
PFaas
  • 19
  • 5
  • [Here's an example of how you could use crossbeam's "scoped threads" feature, as suggested in the answer to the other question.](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c0889a8d3e6c6483317f611a6b365e64) Note that you don't need to manually `join` all the handles when using scoped threads since that is part of how they ensure correctness. – trent May 25 '20 at 02:11

2 Answers2

1

Rust's lifetime system needs to know that if you borrow something (i.e. have a reference to it), that underlying value will exist for the whole time that you borrow it. For regular function calls, this is easy, but threads cause problems here - a thread you start may outlive the function you start it from. So you can't borrow values into a thread, you have to move values into a thread (which is why you have to write thread::spawn(move || { not just thread::spawn(|| {.

When you call .iter() on a Vec, the values the iterator produces are references to the values in the Vec - they're being borrowed. So you can't use them as-is from another thread. You need to move some owned value into the thread.

There are a few ways you can go about this:

If you don't need the Vec after your processing, you could switch to use .into_iter() rather than .iter(). This will iterate over the owned values in the Vec, rather than borrowing them, which means you can move them into the threads. But because the Vec is giving up ownership of the items, your Vec stops being usable after that.

If you do need your Vec after, and your values are clonable (i.e. they implement the Clone trait), you could call .iter().cloned() instead of .iter() - this will make copies of each of the values, which you can then move into the thread.

If you need the Vec afterwards, and either your values aren't clonable, or you don't want to clone them (maybe because it's expensive, or because it matters to you that both threads are using the exact same object), you can have your Vec store Arc<BaseStruct> instead of just BaseStruct - but that's not quite enough - you'll also need to explicitly clone the values before moving them into the thread (perhaps by using .iter().cloned() like above).

A downside of using Arcs is that no threads will be able to modify the values in the future. If you want to use Arcs, but you ever want o be able to modify the values in the future, you'll need instead of storing an Arc<BaseStruct>, to store an Arc<Mutex<BaseStruct>> (or an Arc<RwLock<BaseStruct>>. The Mutex ensures that only one thread can be modifying (or indeed reading) the value at a time, and the Arc allows for the cloning (so you can move a copy into the other thread).

Daniel Wagner-Hall
  • 2,446
  • 1
  • 20
  • 18
  • It's worth noting that you'll run into the same issue where you try to use `self.values` because that relies on being able to borrow `self`, which also won't necessarily outlive the thread. Assuming that's somehow clonable (either because it can be cloned itself, or because it is wrapped in an `Arc`), you can write: `let values = self.values.clone();` inside your `map` closure before your `thread::spawn` closure, and then access `values` in the thread instead of `self.values`. – Daniel Wagner-Hall May 25 '20 at 00:52
  • I like the `.iter().cloned()` option because it leaves the code a bit cleaner, but it runs into a couple other issues. The signature of the closure is finding `for<'r> fn(&'r std::sync::Arc) -> _` but is expecting `fn(std::sync::Arc) -> _`. And the other issue at the tail where `.collect()` is called because the cloned iterator does not result in an `&mut std::iter::Map`. – PFaas May 25 '20 at 11:57
1

I found the issue. Specifically it was the use of self.all_structs.iter().map(). Apparently iter() was requiring static lifetimes.

I switched to the following and it's working now:

fn parallel_calculation(&self) -> f64 {
    let mut handles: Vec<_> = Vec::new();

    for base_struct in &self.all_structs {
        let for_solve = self.for_solve.clone();
        let base_struct = base_struct.clone();

        handles.push(thread::spawn(move || {
            let for_solve = for_solve.read().unwrap();
            base_struct.target_function(&for_solve)
        }));
    };

    let mut result = 0.0;
    for process in handles { result += process.join().unwrap(); };
    return result
}

With Arcs and an RwLock in the main struct as follows:

pub struct StructSet {
    all_structs: Vec<Arc<BaseStruct>>,
    for_solve: Arc<RwLock<HashMap<&'static str, f64>>>,
}
PFaas
  • 19
  • 5
  • It's the `clone`s that fix the problem, not switching from iterators to indexing. Also did you change `base_struct.target_function` to `coefficient.force`? – trent May 25 '20 at 11:58
  • I tried the `clone`s in the `iter().map()` version and it still didn't not work. `iter().map()` didn't even seem to work with an empty closure that just returned 0.0 because of lifetime issues. And good catch on the `base_struct.target_function`. Trying to keep all the hydrodynamics bits out to be less confusing. Edited to fix. – PFaas May 25 '20 at 12:09
  • [For example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=83ed30470fc6c89a6a7caeb5f321de1a). (Note the code in the answer also has a bug in that it uses `handles[i] = ...` instead of `handles.push(...)`; it will panic whenever `all_structs` is non-empty.) – trent May 25 '20 at 12:36
  • [If you wanted to use `.iter().map(...)` you can still do that with the clones inside.](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=bf5e6192d1ab1c21891821e3629f0fb0) Equivalently, you could use `iter().cloned().map(...)` and remove the `base_struct.clone()` line. – trent May 25 '20 at 12:37
  • Updated for `handles.push()`. Much better! – PFaas May 25 '20 at 12:44