portable-interoperable icon indicating copy to clipboard operation
portable-interoperable copied to clipboard

AsyncRead and AsyncWrite

Open nrc opened this issue 3 years ago • 46 comments

Tracking issue for work on AsyncRead and AsyncWrite. The eventual goal is that we have a single, standardised version of these traits in std which are used by all (or at least most mainstream) async runtimes.

Known technical issues:

  • should these traits use poll_read/poll_write functions or async fn read/async fn write
  • how to handle writing to uninitialised memory in AsyncRead
  • how to simultaneously read and write
  • how to do vectored IO
  • working in no_std scenarios (I believe this only requires moving the Error trait to libcore, which is work in progress
  • ensuring these traits work well as trait objects
  • using async drop for shutdown?

There are also several closely related traits such as AsyncSeek, and in various libraries, extension traits and AsyncBufRead.

Current implementations:

Smol re-exports the futures versions.

nrc avatar Dec 07 '21 17:12 nrc

Some resources:

nrc avatar Dec 07 '21 17:12 nrc

  • should these traits use poll_read/poll_write functions or async fn read/async fn write

async fn is better, but it was blocked by async-trait which is unlikely to be stabilized in the short term. 😞

TennyZhuang avatar Dec 07 '21 18:12 TennyZhuang

I think we have a solution to dyn-safe async traits that doesn't require 'inline' async.

nrc avatar Dec 08 '21 14:12 nrc

ReadBuf (RFC 2930) has landed on nightly now (https://github.com/rust-lang/rust/pull/81156)

nrc avatar Dec 09 '21 13:12 nrc

Async-std: Read Write Smol re-exports the futures versions.

Note that async-std also re-exports the futures-io traits, but does so using an alias. This does mean the traits are compatible though. In hindsight we probably should've kept the Async{Read,Write} terminology, but we didn't know that at the time.

yoshuawuyts avatar Dec 14 '21 13:12 yoshuawuyts

Another known blocker that I haven't seen mentioned yet: the working group needs to make a decision on the feasibility of async overloading. This will have consequences for the shape and location of the async IO traits.

Async overloading is being tracked on the roadmap, but does not yet have an initiative owner.

yoshuawuyts avatar Dec 14 '21 13:12 yoshuawuyts

Note that async-std also re-exports the futures-io traits

From the docs, they appear to have different definitions? The async-std versions have significantly more methods. Not sure if it a re-export of an earlier version of the futures definition or something more complex than that.

nrc avatar Dec 16 '21 09:12 nrc

From the docs, they appear to have different definitions? The async-std versions have significantly more methods.

yeah, that touches on another thing we probably shouldn't have done: the methods on async_std::io::{Read,Write} don't actually exist on them; they are only compiled in for the docs, to create the appearance that they do. The way they are made available is by importing async_std::io::prelude::* which includes ReadExt and WriteExt. It's very much a hack, but it allowed us to keep using the futures-io types, while still providing a cohesive feel.

The reason why we did this is because we wanted to push the "async-std is an async version of std" idea as far as we could. We wanted to prove out that it is indeed possible to map std's abstractions and usage patterns 1:1 to async Rust. And it worked; we now know that that it indeed can be done - modulo some missing language features like "async closures", "async drop" and "async traits". But perhaps if we had to try this again, we could've just exposed it as async_std::io::{AsyncRead,AsyncReadExt}. Though obviously that's speaking with the benefit of hindsight.

yoshuawuyts avatar Dec 16 '21 12:12 yoshuawuyts

A proposed design: https://www.ncameron.org/blog/async-read-and-write-traits/

I should flesh out the alternatives with examples and/or why they don't meet the stated goals.

nrc avatar Feb 15 '22 16:02 nrc

Just one comment on the proposed design (which looks fine to me although I'm not really the target audience):

  • For the examples where let mut buf = [0; 1024]; is done after the ready call, I'm trying to figure out how this saves memory, since usually stack space is reserved based on the maximum extent required. Does this depend on buf not carrying over any .await, which means it goes onto the real stack instead of into the coroutine context structure? So the aim is to not have the buffer held across any .await?

uazu avatar Feb 15 '22 20:02 uazu

For the examples where let mut buf = [0; 1024]; is done after the ready call, ...

These are simplified examples, in real life we might use one shared buffer or allocate space on the heap to be used in other functions, etc.

nrc avatar Feb 16 '22 08:02 nrc

Shouldn't AsyncRead and AsyncReady be renamed Read and Ready in the "Complete version" code block ?

bestouff avatar Feb 16 '22 16:02 bestouff

Shouldn't AsyncRead and AsyncReady be renamed Read and Ready in the "Complete version" code block ?

Whoops! They absolutely should. Fixed now, thanks!

nrc avatar Feb 16 '22 17:02 nrc

Feedback from Fuchsia:

The proposal suggests that for optimal performance, Ready should be used followed by a read call later. I think that might be quite challenging for us to implement because it would require locking between the ready notification and the read (to prevent the kernel discarding pages under memory pressure) and AFAICT, there's no indication of how much should be locked in the ready call.

And wonder if we could add a byte count to Interest.

Discussion ongoing on Zulip

nrc avatar Feb 18 '22 09:02 nrc

Some discussion on a possible alternative where we have a type like smol's Async and some of the API is on that type rather than the io traits (I believe the benefit here is that we keep the differences between the sync and async APIs restricted to a single location).

nrc avatar Feb 18 '22 09:02 nrc

The read_with style helpers from smol::Async may also be helpful for making the memory-optimal path more ergonomic in some cases.

nrc avatar Feb 18 '22 09:02 nrc

I've started to write up the designs from the blog posts in https://github.com/nrc/portable-interoperable/tree/master/io-traits

nrc avatar Mar 17 '22 15:03 nrc

Filed https://github.com/nrc/portable-interoperable/issues/7 related to this issue.

yoshuawuyts avatar Mar 17 '22 22:03 yoshuawuyts

The current version of the traits described in the README include vectored methods. In practice, I have found this to be a mistake because it is not possible to have a good default implementation. The user of Read/Write must have a different implementation depending on whether the I/O handle can support vectored ops. What this means in practice, a library like Hyper that takes a T: Read + Write must assume the T does not support vectored ops and avoid calling those methods.

I think, for vectored ops, it should be a separate trait. Converting a T: Read + Write -> VectoredRead + VectoredWrite means wrapping it with a buffer.

carllerche avatar Jun 16 '22 18:06 carllerche

Do you think the

fn is_vectored_writeable() -> bool;

interface that can be found in existing AsyncWrite/AsyncRead/Write/Read solves the problem?

NobodyXu avatar Jun 17 '22 03:06 NobodyXu

Would it work? Possibly. However, once you start adding boolean checks to see if a trait impl supports a feature or not, this seems to strongly suggest there should be two traits.

carllerche avatar Jun 20 '22 16:06 carllerche

That's true.

But if it is split into two traits, then what if the user has an IoSlice and doesn't care about the efficiency anyway?

NobodyXu avatar Jun 21 '22 01:06 NobodyXu

@Noah-Kennedy I wonder is it possible for async fn read to work with io-uring without copying.

Suppose that:

  • The future returned by async fn read under io-uring is lazy and does not actually issue any SQE until it is polled (and pinned).
  • The future returned by async fn read is not Unpin (has PhantomPinned), thus it must be Pined. And Pined object must either be leaked, or must be droped.

With these two assumptions, can we implement async fn read in io-uring without copying it into an internal owned buffer?

NobodyXu avatar Jul 14 '22 02:07 NobodyXu

Nope, because when dropping a future for an in-flight op, it would be unsound still.

Noah-Kennedy avatar Jul 14 '22 02:07 Noah-Kennedy

You can still make this work via IORING_OP_POLL_ADD, which lets you do readiness-based IO via uring.

Noah-Kennedy avatar Jul 14 '22 02:07 Noah-Kennedy

Nope, because when dropping a future for an in-flight op, it would be unsound still.

Can we add a drop implementation that cancels and wait for cancellation/IO completion?

It will be great if we have async drop though.

NobodyXu avatar Jul 14 '22 03:07 NobodyXu

No, because this would block the runtime.

Noah-Kennedy avatar Jul 14 '22 03:07 Noah-Kennedy

No, because this would block the runtime.

Thanks, sounds like this can only be fixed with async drop.

NobodyXu avatar Jul 14 '22 03:07 NobodyXu

Yup

Noah-Kennedy avatar Jul 14 '22 03:07 Noah-Kennedy