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:
Send
The
Send
trait indicates that a value of this type is safe to send from one thread to another.
Sync
The
Sync
trait 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
Sync
by changing its internal structure, e.g. wrapping values in mutexes unsafe impl Sync
on the grounds that in the case aboveStream
requires a&mut self
in itspoll_next
function, 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.