In this reddit discussion I have observed that some people dogmatically believe that keeping Rc across await inevitably makes future !Send, thus making it impossible to migrate its execution across executor threads. It looks like they can not draw direct parallels between execution of threads and tasks, which become quite obvious for me.
My argument is that futures in theory could move between executor threads as freely as threads move between physical cores, even if they contain Rc or any other !Send data. To achieve that we need one simple thing: to enforce that Rc clones do not leave premise of task's state. And this thing is already enforced in APIs for task spawning, sending over channels, and others!
To demonstrate it, I present the following contrived example:
use std::rc::Rc;
pub struct Foo {
a: Rc<u32>,
b: Rc<u32>,
}
impl Foo {
pub fn new(val: u32) -> Self {
let t = Rc::new(val);
Self {
a: t.clone(),
b: t,
}
}
pub fn get_refs(&self) -> (&u32, &u32) {
(&self.a, &self.b)
}
}
unsafe impl Send for Foo {}
Note the last line. We implement Send for a type which contains Rc! Horror!
But think carefully about this code. How sending Foo into a different thread can cause UB (be it using spawn or channels)? We enforce that all copies of Rc are moved together and there is no way to move those outside of Foo and you can not clone Foo itself. Sending value to a different thread inevitable involves memory synchronization. Thus changes on Rc done in thread1 will be properly visible in thread2 to which Foo was sent.
I think it can work similarly with any !Send type, similarly to how we do not care that thread's stack can contain !Send values and move between physical cores. Well... with one caveat: TLS. Luckily the current thread_local! API is limited and should not cause any issues (after all, you can not yield inside closure passed to with). But potential stabilization of #[thread_local] and C libraries which do their own thing may cause issues. Another potential source of issues is reliance on invariants in private TLS, e.g. code put 42 into a private TLS and expects to find it there on next load, using assumption that no other code can access this TLS.
What do you think? Do you agree that example above is sound and that future could migrate to other executor threads even if it keeps Rc and similar types across await?
I do not say that all Futures should suddenly become Sendable, just pointing at the often unnecessarily strictness which only adds to the list of issues of the Rust async system.