I am playing with Rust's concurrency and trying to wrap my head around Send
/Sync
/Arc
/Mutex
. I have problems with sharing a reference to an instance of trait which is held in a HashMap
:
use std::{collections::HashMap, sync::Arc, thread, time::Duration};
#[derive(Debug)]
struct A {
foo: u8,
}
trait Foo {
fn get_foo(&self) -> u8;
}
impl Foo for A {
fn get_foo(&self) -> u8 {
self.foo
}
}
fn main() {
let a = Arc::new(A { foo: 8 });
let mut map: HashMap<u8, Arc<Foo>> = HashMap::new();
map.insert(8u8, a);
for _ in 0..2 {
let a = map.get(&8u8).expect("boom");
let a = a.clone();
thread::spawn(move || {
let _ = a.get_foo();
});
}
thread::sleep(Duration::from_millis(200));
}
(playground)
It gives me these errors:
error[E0277]: `dyn Foo` cannot be sent between threads safely
--> src/main.rs:27:9
|
27 | thread::spawn(move || {
| ^^^^^^^^^^^^^ `dyn Foo` cannot be sent between threads safely
|
= help: the trait `std::marker::Send` is not implemented for `dyn Foo`
= note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<dyn Foo>`
= note: required because it appears within the type `[closure@src/main.rs:27:23: 29:10 a:std::sync::Arc<dyn Foo>]`
= note: required by `std::thread::spawn`
error[E0277]: `dyn Foo` cannot be shared between threads safely
--> src/main.rs:27:9
|
27 | thread::spawn(move || {
| ^^^^^^^^^^^^^ `dyn Foo` cannot be shared between threads safely
|
= help: the trait `std::marker::Sync` is not implemented for `dyn Foo`
= note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<dyn Foo>`
= note: required because it appears within the type `[closure@src/main.rs:27:23: 29:10 a:std::sync::Arc<dyn Foo>]`
= note: required by `std::thread::spawn`
Could anyone please recommend an approach for this task? I think I'm kinda stuck with Rust's way to work with traits and threading.
Rust captures this through the Send and Sync traits. A type is Send if it is safe to send it to another thread. A type is Sync if it is safe to share between threads (T is Sync if and only if &T is Send).
Send means that a type is safe to move from one thread to another. If the same type also implements Copy , this also means that it is safe to copy from one thread to another. Sync means that a type is safe to reference from multiple threads at the same time.
Remember that types of original values which are converted to trait objects are erased. Therefore, the compiler can't know whether the data inside the Arc<Foo>
is Send
and Sync
, and without these traits sharing data across threads may be unsafe. You need to specify that types which can be stored in Arc<Foo>
must be Send
and Sync
:
let mut map: HashMap<u8, Arc<Foo + Sync + Send>> = HashMap::new();
(try it here)
The Send
bound is required by thread::spawn()
, and Sync
is required by Arc
for it to be Send
. Additionally, thread::spawn()
also requires 'static
but it is implicit in this particular Arc<Foo + Sync + Send>
type declaration.
Of course, you will be able to store only Sync
and Send
implementations of Foo
, but this is necessary to ensure memory safety. However, in Rust synchronization is implemented with wrappers like Mutex<T>
or RwLock<T>
. They don't implement Foo
even if T
implements Foo
, therefore you won't be able to store, say, Mutex<Foo + Send>
inside your map (unless Foo
is your trait and you implemented it for Mutex<Foo>
, which could be unwieldy), which would be necessary if your Foo
implementations are not Sync
but Send
(though I'm not sure I can provide an example of such type now).
To solve this you'd need to change map type to contain a mutex inside it explicitly:
let mut map: HashMap<u8, Arc<Mutex<Foo + Send>>> = HashMap::new();
This way, there is no need for the Sync
bound because Mutex
is Sync
if its contents are Send
.
And naturally, you won't be able to share Foo
implementations which are not Send
at all, and there is no way around it. This can happen, for example, if Foo
's implementation contains Rc
s.
Vladimir gives one solution to your problem in the first half of his answer: tell Rust that your HashMap
contains Foo
s which are Send
and Sync
. Alternatively, you may change the definition of Foo
itself to include these trait bounds:
trait Foo: Sync + Send {
fn get_foo(&self) -> u8;
}
Since struct A
is indeed Send
and Sync
, and since struct A
does indeed implement trait Foo
, the type checker will not complain when you use an Arc<A>
as an Arc<Foo>
.
If instead of sharing immutable (atomically reference counted) references to Foo
s you wanted to share mutable (atomically reference counted) references to Foo
s, you need to control access to the Foo
s. This can be accomplished using e.g. a Mutex
. Since the Mutex
would then be taking care of the synchronization, the Sync
bound on Foo
can be dropped. For example:
use std::{
collections::HashMap,
sync::{Arc, Mutex},
thread,
time::Duration,
};
#[derive(Debug)]
struct A {
foo: u8,
}
trait Foo: Send {
fn get_foo(&self) -> u8;
}
impl Foo for A {
fn get_foo(&self) -> u8 {
self.foo
}
}
fn main() {
let a = Arc::new(Mutex::new(A { foo: 8 }));
let mut map: HashMap<u8, Arc<Mutex<Foo>>> = HashMap::new();
map.insert(8u8, a);
for _ in 0..2 {
let a = map.get(&8u8).expect("boom").clone();
thread::spawn(move || {
let result = a.lock().ok().expect("boom indeed").get_foo();
println!("Result: {}", result);
});
}
thread::sleep(Duration::from_millis(200));
}
(playground)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With