I have a struct that is not Send
because it contains Rc
. Lets say that Arc
has too big overhead, so I want to keep using Rc
. I would still like to occasionally Send
this struct between threads, but only when I can verify that the Rc
has strong_count 1 and weak_count 0.
Here is (hopefully safe) abstraction that I have in mind:
mod my_struct {
use std::rc::Rc;
#[derive(Debug)]
pub struct MyStruct {
reference_counted: Rc<String>,
// more fields...
}
impl MyStruct {
pub fn new() -> Self {
MyStruct {
reference_counted: Rc::new("test".to_string())
}
}
pub fn pack_for_sending(self) -> Result<Sendable, Self> {
if Rc::strong_count(&self.reference_counted) == 1 &&
Rc::weak_count(&self.reference_counted) == 0
{
Ok(Sendable(self))
} else {
Err(self)
}
}
// There are more methods, some may clone the `Rc`!
}
/// `Send`able wrapper for `MyStruct` that does not allow you to access it,
/// only unpack it.
pub struct Sendable(MyStruct);
// Safety: `MyStruct` is not `Send` because of `Rc`. `Sendable` can be
// only created when the `Rc` has strong count 1 and weak count 0.
unsafe impl Send for Sendable {}
impl Sendable {
/// Retrieve the inner `MyStruct`, making it not-sendable again.
pub fn unpack(self) -> MyStruct {
self.0
}
}
}
use crate::my_struct::MyStruct;
fn main() {
let handle = std::thread::spawn(|| {
let my_struct = MyStruct::new();
dbg!(&my_struct);
// Do something with `my_struct`, but at the end the inner `Rc` should
// not be shared with anybody.
my_struct.pack_for_sending().expect("Some Rc was still shared!")
});
let my_struct = handle.join().unwrap().unpack();
dbg!(&my_struct);
}
I did a demo on the Rust playground.
It works. My question is, is it actually safe?
I know that the Rc
is owned only by a single onwer and nobody can change that under my hands, because it can't be accessed by other threads and we wrap it into Sendable
which does not allow access to the contained value.
But in some crazy world Rc
could for example internally use thread local storage and this would not be safe... So is there some guarantee that I can do this?
I know that I must be extremely careful to not introduce some additional reason for the MyStruct
to not be Send
.
The raw pointer types are not Send because Rust cannot make any guarantees about the synchronization mechanisms that are in the underlying types, or when the pointers will be accessed. Rc is not Send because it uses a non-atomic reference counter which is not safe to send between threads.
'Arc' stands for 'Atomically Reference Counted'. The type Arc<T> provides shared ownership of a value of type T , allocated in the heap. Invoking clone on Arc produces a new Arc instance, which points to the same allocation on the heap as the source Arc , while increasing a reference count.
An integer the size of which is arch will be 32 bits on an x86 machine and 64 bits on an x64 machine.
No.
There are multiple points that need to be verified to be able to send Rc
across threads:
Rc
or Weak
) sharing ownership.Rc
must be Send
.Rc
must use a thread-safe strategy.Let's review them in order!
Guaranteeing the absence of aliasing
While your algorithm -- checking the counts yourself -- works for now, it would be better to simply ask Rc
whether it is aliased or not.
fn is_aliased<T>(t: &mut Rc<T>) -> bool { Rc::get_mut(t).is_some() }
The implementation of get_mut
will be adjusted should the implementation of Rc
change in ways you have not foreseen.
Sendable content
While your implementation of MyStruct
currently puts String
(which is Send
) into Rc
, it could tomorrow change to Rc<str>
, and then all bets are off.
Therefore, the sendable check needs to be implemented at the Rc
level itself, otherwise you need to audit any change to whatever Rc
holds.
fn sendable<T: Send>(mut t: Rc<T>) -> Result<Rc<T>, ...> {
if !is_aliased(&mut t) {
Ok(t)
} else {
...
}
}
Thread-safe Rc
internals
And that... cannot be guaranteed.
Since Rc
is not Send
, its implementation can be optimized in a variety of ways:
Box
.This is not the case at the moment, AFAIK, however the API allows it, so the next release could definitely take advantage of this.
What should you do?
You could make pack_for_sending
unsafe
, and dutifully document all assumptions that are counted on -- I suggest using get_mut
to remove one of them. Then, on each new release of Rust, you'd have to double-check each assumption to ensure that your usage if still safe.
Or, if you do not mind making an allocation, you could write a conversion to Arc<T>
yourself (see Playground):
fn into_arc(this: Rc) -> Result<Arc, Rc> { Rc::try_unwrap(this).map(|t| Arc::new(t)) }
Or, you could write a RFC proposing a Rc <-> Arc
conversion!
The API would be:
fn Rc<T: Send>::into_arc(this: Self) -> Result<Arc<T>, Rc<T>>
fn Arc<T>::into_rc(this: Self) -> Result<Rc<T>, Arc<T>>
This could be made very efficiently inside std
, and could be of use to others.
Then, you'd convert from MyStruct
to MySendableStruct
, just moving the fields and converting Rc
to Arc
as you go, send to another thread, then convert back to MyStruct
.
And you would not need any unsafe
...
The only difference between Arc
and Rc
is that Arc
uses atomic counters. The counters are only accessed when the pointer is cloned or dropped, so the difference between the two is negligible in applications which just share pointers between long-lived threads.
If you have never cloned the Rc
, it is safe to send between threads. However, if you can guarantee that the pointer is unique then you can make the same guarantee about a raw value, without using a smart pointer at all!
This all seems quite fragile, for little benefit; future changes to the code might not meet your assumptions, and you will end up with Undefined Behaviour. I suggest that you at least try making some benchmarks with Arc
. Only consider approaches like this when you measure a performance problem.
You might also consider using the archery
crate, which provides a reference-counted pointer that abstracts over atomicity.
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