Sharing a struct with a `dyn` field between threads

I'm not sure if I came up with a good enough name.

I have the following situation:

trait Foo {
    fn print(&self);
}

struct FooStruct {
    x: u32,
}

impl Foo for FooStruct {
    fn print(&self) {
        println!("{}", self.x);
    }
}

struct Bar<'a> {
    f: &'a mut dyn Foo,
}

impl<'a> Bar<'a> {
    fn new(f: &'a mut Foo) -> Self {
        Self { f }
    }
}

How can I share Bar between threads?

Base on what I've read so far I should be able to do it using Arc and Mutex, but if I try it like this I can't because dyn Foo cannot be sent between threads safely:

fn main() {
    let mut foo = FooStruct {x : 0};
    let bar = Bar::new(&mut foo);

    let arc = Arc::new(Mutex::new(bar));
    let arc_for_thread = Arc::clone(&arc);

    thread::spawn(|| {
        let myb = arc_for_thread.lock().unwrap();

    });
}

I don't understand what is the right way of doing this. I was under the impression that I can wrap a type that can't be safely shared between threads in an Arc<Mutex<>> and it would be safe.

A Mutex can only help you if the type is Send + !Sync. If it isn't Send, it can't do anything. Change Bar to this:

struct Bar<'a> {
    f: &'a mut dyn Foo + Send,
}

You can use the type dyn Foo + Send to restrict the trait object to types that can be used on other threads.

Also, if you are using the standard library's threads, which can outlive the function that spawns them, you won't be able to share any type that contains references to local variables on the spawning thread (any non-'static type). Wrapping a reference type in Arc can't extend its lifetime; this only works for owned or 'static types.

You could replace the &mut dyn Foo + Send with Box<dyn Foo + Send>, but placing a Box inside an Arc adds an extra layer of indirection that you might not want. Another approach is to put the Arc<Mutex<T>> inside of Bar rather than around it:

use std::sync::{Arc, Mutex};
use std::thread;

trait Foo {
    fn print(&self);
}

struct FooStruct {
    x: u32,
}

impl Foo for FooStruct {
    fn print(&self) {
        println!("{}", self.x);
    }
}

#[derive(Clone)]
struct Bar {
    f: Arc<Mutex<dyn Foo + Send>>,
}

impl Bar {
    fn new(f: impl Foo + Send + 'static) -> Self {
        Self { f: Arc::new(Mutex::new(f)) }
    }
}

fn main() {
    let foo = FooStruct {x : 0};
    let bar = Bar::new(foo);
    let bar_for_thread = bar.clone();

    thread::spawn(move || {
        let myb = bar_for_thread.f.lock().unwrap();
    });
}
2 Likes

This is what I ended up doing. I realized that in my particular situation I don't need the entire Bar to be shared, but only a small sub part of it, so I shared only what was actually needed.

I think I understand. Thank you.

Why does Mutex need the type to be Send ? I'm using the Mutex in order to guard access to whatever it holds, so why the restriction? Can you point me to some parts of the docs or articles that I can read on the subject?

If you may not access a value from other threads than the one it originated in, then it doesn't matter if you try to do so through a mutex. A mutex doesn't change the fact that it's a different thread, and different threads are not allowed at all for non-Send types.

Here's the documentation for Send:

Types that can be transferred across thread boundaries.

This trait is automatically implemented when the compiler determines it's appropriate.

An example of a non- Send type is the reference-counting pointer rc::Rc . If two threads attempt to clone Rc s that point to the same reference-counted value, they might try to update the reference count at the same time, which is undefined behavior because Rc doesn't use atomic operations. Its cousin sync::Arc does use atomic operations (incurring some overhead) and thus is Send .

See the Nomicon for more details.

1 Like

I was wrongly thinking that Arc should fix this, but I looked again at the docs:

The type Arc<T> provides shared ownership of a value of type T , allocated in the heap.

So I need to Box things if I want to do that. This starts to make sense now.

You need to Box it to do what? Fundamentally you just can't move a non-Send type across threads.

For everyone who is wondering what can be used in this case instead of the threads from the standard library: scoped threads provide threads that are guaranteed to be joined at the end of the current scope and can receive non static types. There are multiple crates on crates.io that provide scoped threads.

1 Like