AsyncWriteは間違っている
RustのAsyncWrite/AsyncReadトレイトが本質的に非効率である理由
AsyncWriteはこういうやつ。
pub trait AsyncWrite {
// Required methods
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>>;
fn poll_flush(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), Error>>;
fn poll_close(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), Error>>;
// Provided method
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[IoSlice<'_>],
) -> Poll<Result<usize, Error>> { ... }
}
これだとちょっと分かりづらいが、このトレイトが実装されいていると(tokioの場合)AsyncWriteExtで.write(&buf).await?のようなコードが書けるようになる。async-stdとかもこういうノリだった。
非同期I/Oとcancel safety
非同期I/OではI/Oリクエストを投げて、それが完了するのを待つAPIを使う。epollならO_NONBLOCKとかでリクエストを発行してepollで待つし、
図にするとこうなる。async taskが所有するバッファでI/Oを発行し、そのあとI/Oが完了する前にasync taskをdropする。tokio::select!とかでそうした状況が発生するケースがある。cancel safetyと呼ぶ。
単純に動作が狂うとかならまだいいのだが、メモリ安全性が消滅するならそれはunsafeとマークされなければならない。
ではどうすれば良いのか?'qみたいにする。そしてバッファのライフタイムを'bとした時、'b: 'qとなれば良い。
I/Oが完了するまでDropをさせないのようにしても安全にはなる。selectでタイムアウトは出来なくなるが。[u8]を使うのは難しい。バッファを何かしらの型でwrapして、
最も良いのはバッファの所有権ごとキューを管理する構造体にmoveしてしまうことだ。
trait AsyncWrite {
async fn write<B: IoBuffer>(&mut self, buf: B) -> (B, io::Result<usize>);
}
実際、io_uringを使うmonoioはこれとほとんど同じインターフェースになっている。
ではtokioはどうやって参照を取りながらcancel safeかつ安全なasyncwriteを提供しているのか?
まとめ
async-stdはstdをそのままasyncに置き換えることを目標としていた。tokioにおいても大筋は似たようなインターフェース設計がなされている。
個人の意見としてはasync周りについては、Wakerがマルチスレッド前提であることも含めて失敗が多いと思う。
コメント
コメントはまだありません