We recently tripped over the Sync requirement for the argument passed to hyper’s Body::wrap_stream constructor: normally, there’s no reason for Stream to be Sync. A Stream describes a task to be submitted to an executor which will then poll until finished, possibly from different threads, but never concurrently. In contrast, the Body for an HTTP response in hyper needs to be Sync due to async/await: awaiting inside a match block captures borrow too eagerly · Issue #57017 · rust-lang/rust · GitHub and related behavior.
To make the long story short: handling &Body in an async fn requires them to be Send if held over an .await, which requires Body to be Sync, which in turn requires the wrapped Stream to be Sync.
From the documentation:
SendThe
Sendtrait indicates that a value of this type is safe to send from one thread to another.
SyncThe
Synctrait indicates that a value of this type is safe to share between multiple threads.
These make perfect sense and enable Rust’s fearless concurrency, a very important and beneficial feature as I see it (as you may know I’m one of the main authors of Akka and I wish the JVM or the languages atop offered affine types!).
With the introduction of async/await, some usage patterns have manifested that warrant a discussion on the exact meaning of the above traits — please let me know if this discussion has already happened elsewhere and I’ll read up on it.
The issue
While Sync means unrestricted sharing of a value between threads, the “sharing” over the lifetime of an async fn body is of a very particular nature: there will be no concurrent access to the value (barring bugs in the underlying async runtime implementation).
In practice I see two ways for users to deal with this right now:
- make the referenced object
Syncby changing its internal structure, e.g. wrapping values in mutexes unsafe impl Syncon the grounds that in the case aboveStreamrequires a&mut selfin itspoll_nextfunction, thereby requiring the caller to guarantee mutual exclusion
Neither of them feels right: the mutex is not actually needed and has a performance overhead (mostly by being an optimization barrier to both the compiler and the CPU), and unsafe should not be required by any end-user code, it should be purely opt-in if you want to optimize something.
So, is there something else that we can do?
Tangential point
&mut is named after “mutation”, but its type-system function is to guarantee exclusivity. Exclusivity is also required for non-mutating access to a shared data structure in order to guarantee consistency, which is the reason for having to take the mutex for just reading from the structure — calling it &mut in the syntax is a bit more confusing than calling it &excl but I think that ship has sailed very long ago ![]()
Why I’m bringing it up is that the &mut fixes the Sync issue for Stream, at least to some degree, but not all users will understand this point without help.