`StreamExt::split` docs are misleading — it’s not truly concurrent and serializes reads/writes via BiLock
Summary
The documentation for StreamExt::split currently implies that splitting enables concurrent use of a Stream + Sink object across tasks.
However, the implementation uses a BiLock internally, which serializes access — meaning reads and writes do not actually occur concurrently.
This behavior is correct and safe, but the current docs can easily be misinterpreted as offering true full-duplex concurrency (similar to TcpStream::into_split).
Example of the confusion
A user might write code like this:
let (mut sink, mut stream) = framed.split();
tokio::spawn(async move {
sink.send("ping").await.unwrap();
});
tokio::spawn(async move {
while let Some(msg) = stream.next().await {
println!("got: {:?}", msg);
}
});
This compiles, but both halves share a BiLock, so internally:
- Only one half can hold the lock at a time.
- Reads and writes are serialized at the
poll_*level. - The two tasks effectively take turns polling the same underlying resource.
So, while ownership is “split,” execution is not truly concurrent.
Why this matters
-
For transports like
TcpStream, WebSockets, or any full-duplex I/O, this distinction is performance-significant. Developers often expect the split halves to operate independently, as in Tokio’s TcpStream split API. -
For higher-level protocol abstractions, serialized polling can lead to unintuitive interleaving, where a read may occur between partial writes. This isn’t a safety issue, but it can surprise users who assume atomic or parallel I/O behavior.
Suggested fix
Add a short note section in the docstring clarifying that StreamExt::split is not concurrent and uses a BiLock internally.
Something along these lines:
Note: This split does not enable true concurrent reading and writing. Both halves share internal state protected by a [
BiLock], so reads and writes occur serially rather than in parallel. If you require full-duplex I/O, consider splitting the underlying I/O if it supports true concurrency
This would help set correct expectations without changing any behaviour.
Why this is safe to add
- It doesn’t alter semantics or API surface.
- It prevents common performance misunderstandings.
- It helps explicitly distinguish logical vs physical concurrency.
References
Willing to contribute
I’d be happy to open a small PR to update the docs if maintainers agree that clarification would be helpful.