Code Monkey
Code Monkey

Reputation: 11

How do you pass a struct that contains a String reference between threads?

In the following code snippet I am trying to create a tuple that contains a String value and a struct that contains an attribute that is set as the reference to the first value (which would be the String) in the tuple.
My question is how do I make the String (which is the first value in the tuple) live long enough.

Here is the code:

#[derive(Debug, Clone)]
struct RefTestStruct {
    key: usize,
    ref_value: Option<&'static str>,
}

fn init3<'a>( cache: &Arc<Mutex<HashMap<usize, (String, Option<RefTestStruct>)>>> ) {
    let mut handles: Vec<JoinHandle<()>> = vec![];
    for idx in 0..10 {
        //create reference copy of cache
        let cache_clone = Arc::clone(cache);

        let handle = thread::spawn(move || {
                //lock cache
                let mut locked_cache = cache_clone.lock().unwrap();

                // add new object to cache
                let tuple_value = (format!("value: {}", idx), None );
                let mut ts_obj = 
                    RefTestStruct {
                        key: idx,
                        ref_value: Some( tuple_value.0.as_str() ),
                    };
                tuple_value.1 = Some(ts_obj);
                locked_cache.insert(idx as usize, tuple_value);
    
                println!("IDX: {} - CACHE: {:?}", idx, locked_cache.get( &(idx as usize) ).unwrap() )
            });

        handles.push(handle);

    }

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

    println!("\n");
}

fn main() {
    // init
    let cache = HashMap::<usize, (String, Option<RefTestStruct>)>::new();
    let am_cache = Arc::new(Mutex::new(cache));

    init3(&am_cache);

    // change cache contents
    let mut handles: Vec<JoinHandle<()>> = vec![];
    for idx in 0..10 {
        let cache_clone = Arc::clone(&am_cache);

        let handle = thread::spawn(move || {
                let mut locked_cache = cache_clone.lock().unwrap();
                let tuple_value = locked_cache.get_mut( &(idx as usize) ).unwrap();
                (*tuple_value).0 = format!("changed value: {}", (*tuple_value).1.unwrap().key + 11);
                let ts_obj = 
                    RefTestStruct {
                        key: (*tuple_value).1.unwrap().key,
                        ref_value: Some( (*tuple_value).0.as_str() ),
                    };
                tuple_value.1 = Some(ts_obj);

                // locked_cache.insert(idx as usize, tuple_value);

            });
        handles.push(handle);

    }

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

    // display cache contents
    let mut handles: Vec<JoinHandle<()>> = vec![];
    for idx in 0..10 {
        let cache_clone = Arc::clone(&am_cache);

        let handle = thread::spawn(move || {
                let locked_cache = cache_clone.lock().unwrap();
                let ts_obj = locked_cache.get( &(idx as usize) ).unwrap();

                println!("IDX: {} - CACHE: {:?}", idx, &ts_obj );

            });
        handles.push(handle);

    }

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

}

ERROR:

error[E0597]: `tuple_value.0` does not live long enough
   --> src\main.rs:215:42
    |
215 |                         ref_value: Some( tuple_value.0.as_str() ),
    |                                          ^^^^^^^^^^^^^^^^^^^^^^
    |                                          |
    |                                          borrowed value does not live long enough
    |                                          argument requires that `tuple_value.0` is borrowed for `'static`
...
221 |             });
    |             - `tuple_value.0` dropped here while still borrowed

Upvotes: 0

Views: 1313

Answers (1)

typetetris
typetetris

Reputation: 4867

How do you pass a struct that contains a String reference between threads?

First I answer the question in the caption and ignore the program you posted (And I'll try to ignore my itch to guess what you really want to do.)

I'll assume, you want to avoid unsafe in your own code. (So dependencies with unsafe code might be ok.)

To pass a reference to a thread, the reference has to live as long as the thread will live.

Your options:

  1. Have a 'static lifetime on your reference.
  2. Use threads which have a non 'static lifetime.

Regarding 1. have a 'static lifetime on the reference.

So now the questions changed into, how to obtain a reference to a String with 'static lifetime.

We can't use a constant, as we can't initialize it.

This leaves us with the possibility to Box the String and leak it.

But of course, this leaks memory, as the String can't be dropped any more. Also you get a &'mut String out of it, so only one thread can have it at a time and after being done with it, pass it on. So to me it looks like it is essentially the same you could do with the owned value of type String too, so why bother and risk the memory leak. (Putting a Arc<Mutex<..>> around it, is also "same you coud do with the owned value".).

Other methods might exist, which I am not aware of.

Regarding 2. Use threads which have a non `'static' lifetime.

These are called scoped threads and guarantee, that the thread exits, before the lifetime ends. These are not in standard rust yet, but there are crates implementing those (I guess using unsafe).

For example crossbeam offers an implementation of this.

Looking at your program though, I guess you want to have long lived threads.

The program you posted and what you probably want to achieve.

The program you posted has a different issue besides.

You try to create a self referential value in your cache. As far as I know, this is not possible in rust, without using a shared ownership tool, like Arc.

If you try to use a plain Arc, the access would be read only and users of the cache could only change, which String the cache entry points to, but not the "pointed to" String itself.

So If one user replaced the Arc<String> in the cache, other threads would still have a reference to the "old" String and don't see the changed value. But I guess that is what you want to achieve.

But that is a conceptual problem, you can't have unsynchronized read access to a value, which can then be mutated concurrently (by whatever means).

So, if you want to have the ability to change the value with all prior users being able to see it, this leaves you with Mutex or RWLock depending on the access pattern, you are anticipating.

A solution using Arc<String> with the aforementioned defects:

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};

#[derive(Debug, Clone)]
struct RefTestStruct {
    key: usize,
    _ref_value: Arc<String>,
}

type Cache = HashMap<usize, (Arc<String>, RefTestStruct)>;
type AmCache = Arc<Mutex<Cache>>;

fn init3(cache: &AmCache) {
    let mut handles: Vec<JoinHandle<()>> = vec![];
    for idx in 0..10_usize {
        //create reference copy of cache
        let cache_clone = Arc::clone(cache);

        let handle = thread::spawn(move || {
            //lock cache
            let mut locked_cache = cache_clone.lock().unwrap();

            // add new object to cache
            let s = Arc::new(format!("value: {}", idx));
            let ref_struct = RefTestStruct {
                key: idx,
                _ref_value: s.clone(),
            };
            let tuple_value = (s, ref_struct);
            locked_cache.insert(idx, tuple_value);

            println!(
                "IDX: {} - CACHE: {:?}",
                idx,
                locked_cache.get(&idx).unwrap()
            )
        });

        handles.push(handle);
    }

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

    println!("\n");
}

fn main() {
    // init
    let cache = Cache::new();
    let am_cache = Arc::new(Mutex::new(cache));

    init3(&am_cache);

    // change cache contents
    let mut handles: Vec<JoinHandle<()>> = vec![];
    for idx in 0..10_usize {
        let cache_clone = Arc::clone(&am_cache);

        let handle = thread::spawn(move || {
            let mut locked_cache = cache_clone.lock().unwrap();
            let tuple_value = locked_cache.get_mut(&idx).unwrap();
            let new_key = tuple_value.1.key + 11;
            let new_s = Arc::new(format!("changed value: {}", new_key));
            (*tuple_value).1 = RefTestStruct {
                key: new_key,
                _ref_value: new_s.clone(),
            };
            (*tuple_value).0 = new_s;
        });
        handles.push(handle);
    }

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

    // display cache contents
    let mut handles: Vec<JoinHandle<()>> = vec![];
    for idx in 0..10_usize {
        let cache_clone = Arc::clone(&am_cache);

        let handle = thread::spawn(move || {
            let locked_cache = cache_clone.lock().unwrap();
            let ts_obj = locked_cache.get(&idx).unwrap();

            println!("IDX: {} - CACHE: {:?}", idx, &ts_obj);
        });
        handles.push(handle);
    }

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

I am sure it is not difficult to derive the Arc<Mutex<String>> version from that, if desired.

That would not be as pointless, as one might think. The Mutex around the cache protects the HashMap for insertion and deletion. And the Mutex around the String values, protects the string values themselves and can be locked indepently of the Mutex around the HashMap. Usual caveats apply, if you ever need both locks at the same time, lock them in the same order always, or risk a deadlock.

Upvotes: 2

Related Questions