The `Sync` bound nobody asked for
11 points by gendx
11 points by gendx
What?
There are a couple factoids here that are correct:
&self.&Self: Send requires Self: Sync, which is "more restrictive" than &mut Self: Send, even though exclusive references are seen as "more restrictive".I can see how this can be confusing at first, but both facts combine to produce correct behavior. The Sync requirement is needed. The title & muddied conclusion of the post suggest otherwise, which is wrong.
author of the post here. what I meant to say in this post is that while there is no explicit Sync requirement (unlike an explicit Send requirement by tokio::spawn) simply having a &self receiver in an async method of a trait that returns a Send future now requires Self: Sync (and that's where "nobody asked for" comes from in the post's title - since Sync bound is implicit and comes from &self: Send -> Self: Sync). and then if we would want to add interior mutability (actual situation that prompted this post) which would render Self: !Sync we would either have to: a) use synchronization primitives (e.g mutex/atomics) instead or b) simply change &self -> &mut self which drops Sync requirement.
That is a much better summary, thanks! "there is no explicit Sync requirement" is the important part, resulting from weird interaction between Send/Sync & the async system. All in all the blog post is a good demonstration of how this arises, sorry if I was too harsh on it.
Hard to interpret, but I think this
But we’ve added synchronisation overhead on every state access for a worker whose state is only ever touched from inside a single spawned task.
points to the same sort of surprise as discussed in https://matklad.github.io/2023/12/10/nsfw.html
I'm not quite convinced by the Cell example without context.
The whole point of types in the std::cell family is to be able to mutate them via a &self (interior mutability). So it's fair that they aren't Sync - and if you want both interior mutability and Sync it's fair that you need a Mutex or similar.
But if you can pass around a &mut self at will, you shouldn't need any of these interior mutability wrappers in the first place!
So without a more concrete example, I think the title could have been "The Cell nobody asked for". And indeed, it's quite common to tell Rust beginners to just "Arc<Mutex<_>> and chill" to not have to worry about the borrow checker. But if you're at a stage where performance matters and you indeed could have afforded to pass around a &mut self, it's time to remove the cell/mutex and "fight the borrow checker" a bit. Or use a non-work-stealing async runtime that doesn't send things across threads. Or any other applicable optimization.
I think the part left unsaid is that they can pass the mutable reference in places they need to call the asyn trait methods but not necessarily everywhere else.
author of the post here. I agree that the example in the post is a bit weak precisely for the reason you mentioned - if I can afford to just swap &self for &mut self then what's the point of using Cell at all? the actual situation that prompted this post was a bit more involved. there was a helper struct that was using interior mutability and it got added to Self thus rendering Self: !Sync. And the solution to the issue was to switch from &self to &mut self and still continue using that struct with interior mutability unchanged. so the fact that in this situation switching from &self to &mut self lifts (arguably overly restrictive) Self: Sync requirement is the main point of the post