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 Send
able, just pointing at the often unnecessarily strictness which only adds to the list of issues of the Rust async
system.