How to return a reference to `thread_local!` value?

Suppose I want to have exactly one instance of my struct for each thread.

Instances should be created lazily.

Is it possible to manage those instances with thread_local! and return a &'static reference?

I can't seem to get the lifetimes right in function thread_instance():

thread_local! { 
    static INSTANCE: MyStruct = MyStruct::new();
}

pub struct MyStruct {
    /* ... */
}

impl MyStruct {
    /// Return instance for current thread
    /// Should be: Created "on demand" when accessed for the first time
    pub fn thread_instance() -> &'static Self {
        INSTANCE.with( /* ... */ ) // <-- what to do here ???
    }

    pub fn new() -> Self {
        /* ... */
    }
}

Each threat will have one "static" instance, so getting a &'static ref should be possible? :thinking:

I think we could use Rc<T> here, but it kind of contradicts using thread_local! to create a separate "static" instance for each thread. Is there a better solution for this kind problem ???

Thank you!

1 Like

Use Rc<T> or provide an API that accepts a callback, anything else is unsound.

rand crate in the past provided a reference to an user wrapped in structure that is !Send + !Sync, but they moved to Rc<T> when it was pointed out it was unsound in presence of other thread locals with destructors (https://github.com/rust-random/rand/issues/968).

3 Likes

The very point of the callback-based API of LocalKey is that you can't return a reference to outside the function. The reason for this is simple: thread locals aren't actually static. They live only as long as the enclosing thread, and no longer. If you could return a &'static reference to a thread-local, then you could do this:

    let handle = std::thread::spawn(|| {
        get_static_ref_to_thread_local()
    });
    let invalid_ref = handle.join().unwrap();

which would cause a dangling reference.

You can observe how it segfaults when unsafely forced: Playground.

4 Likes

Thanks for explanation!

Then what is the standard use-case for thread_local!? I mean, if we ca not return the "thread_local" struct as a reference. And we also do not want to clone() it – because, having exactly one instance per thread is all the reason why we wanted to use thread_local!.

So, wrapping the value in an Rc<T> and returning a clone of the Rc<T> is the way to go, right?


Another question: How to deal with constructors that might fail?

thread_local! { 
    static INSTANCE: Rc<MyStruct> = Rec::new(MyStruct::new().unwrap());
}

impl MyStruct {
    pub fn new() -> Result<Self> {
        /* ... */
    }

    /// This should return Ok() if initialized successfully, otherwise Err()
    pub fn thread_instance() -> Result<Rc<MyStruct>> {
        /* ... ???? ... */
    }

If we don't want to use unwrap() or expect() in the thread_local! block, because they might cause a panic, how to cleanly handle the error?

Just use the thread-local directly - every time you need to do something with it you can run INSTANCE.with(...) to access its state.

There are ways of hiding this using indirection (i.e. objects/functions that hide the with() calls), but at the end of the day, there (by design!) is no way to move ownership of a thread-local variable out of the ThreadHandle or return a reference to its value.

I normally interpret this friction as my code saying I've got a weird dependency that behaves differently to other values and therefore needs to be handled specially. Thread-locals can make things really tricky to debug or test, so it's worth asking whether you need them or how you can best encapsulate the thread-local-ness.

5 Likes

It's for when you want exactly one instance of the thing per thread (or when you really wanted a real static/global, but you can't have one, because the type you are trying to instantiate isn't thread-safe, which Rust requires in statics).

Returning a direct, long-lived reference to a value is not the only way you can use it. The with() API of LocalKey is still useful without Rc. You should first and foremost refactor your code in a way that it's possible to compute any other results you need without a 'static reference, directly from inside the callback passed to with().

3 Likes

Hmm, came up with this solution/workaround:

thread_local! { 
    static INSTANCE: LazyCell<MyStruct> = LazyCell::empty();
}

impl MyStruct {
    pub fn instance() -> Result<Rc<Self>> {
        INSTANCE.with(|val| val.or_init_with(Self::new))
    }

    pub fn new() -> Result<Self> { /* ... This can fail !!! ... */ }
}
use std::rc::Rc;
use std::cell::{RefCell, RefMut};

type Value<T> = Option<Rc<T>>;

pub struct LazyCell<T> {
    container: RefCell<Value<T>>,
}

impl<T> LazyCell<T> {
    pub fn empty() -> Self {
        Self {
            container: RefCell::new(None),
        }
    }

    pub fn or_init_with<E, F>(&self, init_fn: F) -> Result<Rc<T>, E>
    where
        F: FnOnce() -> Result<T, E>
    {
        Self::init(self.container.borrow_mut(), init_fn)
    }

    fn init<E, F>(mut container: RefMut<Value<T>>, init_fn: F) -> Result<Rc<T>, E>
    where
        F: FnOnce() -> Result<T, E>
    {
        match container.as_ref() {
            Some(existing) => Ok(existing.clone()),
            None => match init_fn() {
                Ok(value) => Ok(container.insert(Rc::new(value)).clone()),
                Err(error) => Err(error),
            }
        }
    }
}

You don't need to re-invent lazy initialization, since thread_local! is already lazy by itself. Here's a much simpler equivalent of your snippet above.

2 Likes

You don't need to re-invent lazy initialization, since thread_local! is already lazy by itself. Here's a much simpler equivalent of your snippet above.

Thanks! But if I get this right, then your example will store the Result in the thread_local! on the very first attempt, regardless of whether it succeeded or failed. And then that's it.

What I had in mind (and tried to implement) is that if the initialization failed, then MyStruct::instance() will return the error, but the thread_local! remains in "uninitialized" state in that case, so that we may retry the initialization when MyStruct::instance() is called again...

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.