Why rust disallow Rc<> to be moved into another thread

use std::thread;
use std::rc::Rc;

fn main() {
    let mut x = Rc::new("0".to_string());
    let t = thread::spawn(move || {
        println!("{}", *x);
    });
}

result in the following error

error[E0277]: `Rc<String>` cannot be sent between threads safely
   --> src/main.rs:6:13
    |
6   |       let t = thread::spawn(move || {
    |  _____________^^^^^^^^^^^^^_-
    | |             |
    | |             `Rc<String>` cannot be sent between threads safely
7   | |         println!("{}", *x);
8   | |     });
    | |_____- within this `[closure@src/main.rs:6:27: 8:6]`
    |
    = help: within `[closure@src/main.rs:6:27: 8:6]`, the trait `Send` is not implemented for `Rc<String>`
    = note: required because it appears within the type `[closure@src/main.rs:6:27: 8:6]`
note: required by a bound in `spawn`

I understand the direct reason is that Rc<> does not implement Send. But the key here is that the closure above has the move keyword before it. My Intuition is that it is safe to move anything into a new thread because nothing is shared between these threads. Is my intuition wrong or the Send/Sync mechanism just doesn't allow this correct code ?

The reference counter is shared, so the implementation of the increment-decrement mechanism has to be thread safe in order to use a refcounted pointer from multiple threads. That has runtime overhead which might be undesirable, so Rc doesn't use synchronization.

If you want a thread-safe refcounted pointer, use Arc.

1 Like

In this particular case, though, there's no actual sharing: the Rc hasn't yet been cloned. Unfortunately, that sort of reasoning is too fine-grained for the type system to be able to handle.

A small refactoring to construct the Rc in the new thread lets the code work, however:

use std::thread;
use std::rc::Rc;

fn main() {
    let x = "0".to_string();
    let t = thread::spawn(move || {
        let x = Rc::new(x);
        println!("{}", *x);
    });
    t.join().unwrap();
}
2 Likes

As I understood, move in rust is just a shallow copy operation plus invalidating old value. In above code the closure has the move keyword before it, so when the closure is being created, the captured x is also created, by shallow copying and invalidating outer x. This all happens before new thread is spawn. Now there is only one single valid Rc<>, inside the closure, so nothing will be shared. Please point out my mistake.

In the general case, an owned Rc<T> likely has a shared reference count with other owned Rc<T> instances. A function like this cannot be valid, for instance, as x may have been cloned from some other Rc<String>:

fn spawn_print(x: Rc<String>) {
    let _ = thread::spawn(move || {
        println!("{}", *x);
    });
}

Rust's type / trait system can't tell the difference between this and what you wrote, and so disallows your code to ensure that functions like spawn_print are always rejected.

1 Like

But that isn't a property of Rc! The Rc type knows nothing about thread::spawn and it is completely agnostic of when any sort of independent function is called. You can't really encode that sort of property in the type system (at least not easily – you could do it with typestate AFAICT). All Send means is "is this type thread-safe?", for which the answer is "No, Rc is not thread safe".

I would say it's the latter. Rc is !Send specifically so this kind of code cannot compile, because Rc is not thread-safe.

edit: But also it's the former, and the code is not correct, so :person_shrugging:

You can express this fact by not wrapping the object behind Rc<> until it actually needs to be shared.

2 Likes

Note that more generally, if you have an Rc<T> and you are sure that it has no other outstanding strong references, then you can use Rc::try_unwrap to extract the inner value T. If T: Send, then you can pass it into another thread.

2 Likes

In order to avoid the overhead of unwrapping and re-wrapping an Rc like that, one could also implement a safe wrapper-type for “uniquely owned Rcs”, e.g. like this:

Rust Playground

Such a type can soundly implement Send, but also Sync or DerefMut, or an infallible into_inner method. It’s basically like a Box<T>, but cheaply convertible to/from Rc.

6 Likes

I can't personally vouch for the rc-box crate, since I've never used it, but I believe this is its purpose.

3 Likes

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.