1

This is an experiment I'm doing while learning Rust and following Programming Rust.

Here's a link to the code in the playground.

I have a struct (Thing) with some inner state (xs). A Thing should be created with Thing::new and then started, after which the user should choose to call some other function like get_xs.

But! In start 2 threads are spawned which call other methods on the Thing instance that could mutate its inner state (say, add elements to xs), so they need a reference to self (hence the Arc). However, this causes a lifetime conflict:

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> src/main.rs:18:30
   |
18 |         let self1 = Arc::new(self);
   |                              ^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined 
on the method body at 17:5...
  --> src/main.rs:17:5
   |
17 | /     fn start(&self) -> io::Result<Vec<JoinHandle<()>>> {
18 | |         let self1 = Arc::new(self);
19 | |         let self2 = self1.clone();
20 | |
...  |
33 | |         Ok(vec![handle1, handle2])
34 | |     }
   | |_____^
note: ...so that expression is assignable (expected &Thing, found &Thing)
  --> src/main.rs:18:30
   |
18 |         let self1 = Arc::new(self);
   |                              ^^^^
   = note: but, the lifetime must be valid for the static lifetime...
note: ...so that the type `[closure@src/main.rs:23:20: 25:14 
self1:std::sync::Arc<&Thing>]` will meet its required lifetime bounds
  --> src/main.rs:23:14
   |
23 |             .spawn(move || loop {
   |              ^^^^^

Is there a way of spawning the state-mutating threads and still give back ownership of thing after running start to the code that's using it?

use std::io;
use std::sync::{Arc, LockResult, RwLock, RwLockReadGuard};
use std::thread::{Builder, JoinHandle};

struct Thing {
    xs: RwLock<Vec<String>>
}

impl Thing {

    fn new() -> Thing {
        Thing {
            xs: RwLock::new(Vec::new()),
        }
    }

    fn start(&self) -> io::Result<Vec<JoinHandle<()>>> {
        let self1 = Arc::new(self);
        let self2 = self1.clone();

        let handle1 = Builder::new()
            .name("thread1".to_owned())
            .spawn(move || loop {
                 self1.do_within_thread1();
            })?;

        let handle2 = Builder::new()
            .name("thread2".to_owned())
            .spawn(move || loop {
                self2.do_within_thread2();
            })?;

        Ok(vec![handle1, handle2])
    }

    fn get_xs(&self) -> LockResult<RwLockReadGuard<Vec<String>>> {
        return self.xs.read();
    }

    fn do_within_thread1(&self) {
        // read and potentially mutate self.xs
    }

    fn do_within_thread2(&self) {
        // read and potentially mutate self.xs
    }
}

fn main() {
    let thing = Thing::new();
    let handles = match thing.start() {
        Ok(hs) => hs,
        _ => panic!("Error"),
    };

    thing.get_xs();

    for handle in handles {
        handle.join();
    }
}
David Castillo
  • 4,266
  • 4
  • 23
  • 25
  • 2
    _"I think I get what it's saying. The self references - through the Arc - could live longer than self itself"_ Not at all! `Arc::new(self)` is creating a value of type `Arc<&Self>`, which is still tied to the internal reference's lifetime. A method taking `&self` cannot magically make a value live longer. [This question](https://stackoverflow.com/q/42028470/1233251) might be useful. – E_net4 Mar 28 '18 at 09:15
  • @E_net4 I'll just remove that part from the question. I didn't mean what you understood haha, and it's not relevant to the discussion. I still have to wrap my mind around lifetimes. – David Castillo Mar 28 '18 at 13:48
  • @E_net4 I refactored the code according to your suggestions to make it less distracting. The compiler error message and the playground link were updated also. – David Castillo Mar 28 '18 at 14:17

1 Answers1

1

The error message says that the value passed to the Arc must live the 'static lifetime. This is because spawning a thread, be it with std::thread::spawn or std::thread::Builder, requires the passed closure to live this lifetime, thus enabling the thread to "live freely" beyond the scope of the spawning thread.

Let us expand the prototype of the start method:

fn start<'a>(&'a self: &'a Thing) -> io::Result<Vec<JoinHandle<()>>> { ... }

The attempt of putting a &'a self into an Arc creates an Arc<&'a Thing>, which is still constrained to the lifetime 'a, and so cannot be moved to a closure that needs to live longer than that. Since we cannot move out &self either, the solution is not to use &self for this method. Instead, we can make start accept an Arc directly:

fn start(thing: Arc<Self>) -> io::Result<Vec<JoinHandle<()>>> {
    let self1 = thing.clone();
    let self2 = thing;

    let handle1 = Builder::new()
        .name("thread1".to_owned())
        .spawn(move || loop {
             self1.do_within_thread1();
        })?;

    let handle2 = Builder::new()
        .name("thread2".to_owned())
        .spawn(move || loop {
            self2.do_within_thread2();
        })?;

    Ok(vec![handle1, handle2])
}

And pass reference-counted pointers at the consumer's scope:

let thing = Arc::new(Thing::new());
let handles = Thing::start(thing.clone()).unwrap_or_else(|_| panic!("Error"));

thing.get_xs().unwrap();

for handle in handles {
    handle.join().unwrap();
}

Playground. At this point the program will compile and run (although the workers are in an infinite loop, so the playground will kill the process after the timeout).

E_net4
  • 27,810
  • 13
  • 101
  • 139