1 ポイント 投稿者 GN⁺ 2024-07-22 | 1件のコメント | WhatsAppで共有
  • RustのPinは、async/awaitが生成するFutureの中で自己参照状態を安全に扱うために導入された基盤要素である
  • async Futureはawait地点ごとに状態を保存するため、1つのオブジェクト内のフィールドが同じオブジェクトの別のフィールドを参照する自己参照型になり得る
  • move constructor、offset pointer、?Move設計は、それぞれランタイム追跡コスト、コンパイル可能性、既存APIとの後方互換性の問題により採用されなかった
  • 最終的な設計は、ポインタを包んで対象をpinned typestateに置くPinであり、Unpin auto traitのおかげで大半の型は従来どおり移動可能である
  • Pinの難しさは不変性の概念自体よりもライブラリ型の限界に由来し、reborrowing、Pin::set、pinned projection、Dropとの相互作用が使い勝手を大きく下げている

Pinが必要になる問題

  • RustのasyncエコシステムにおいてPinpinningは中核的な基盤だが、async Rustを学ぶ人にとって依然として難しく、誤解の多い領域である
  • Pinの目的は、安全なRustだけでユーザーが直接自己参照型を作れるようにすることではない
    • コンパイラがasync関数から生成した自己参照Futureや、tokioのようなランタイムがunsafeコードで作成した自己参照型を安全に操作できるようにすることにある
  • 例としてasync fn barでは、foo(&mut z).awaitの地点でzと、zを参照するFoo Futureを同じFuture状態の中に一緒に保存する必要がある
    • このときFutureオブジェクト内部のあるフィールドが、同じオブジェクト内の別のフィールドを参照する
    • このようなFuture型は自己参照型になる
  • オブジェクトがこの状態に入った後で移動すると、内部参照は以前のメモリ位置を指したままになり、その場所は無効なメモリであったり別の値に再利用されたりする可能性がある
  • Pin以前のRustでは、所有権を持っているかmutable referenceを持っていればオブジェクトを移動できたため、ある時点以降は移動禁止であることを表現する方法が必要だった

解決策にならなかったアプローチ

  • move constructor

    • move constructorは、値が移動されるときにdestructorのようにコードを実行し、自己参照ポインタを新しい位置に修正する方式である
    • Rustではポインタは移動される値の「中」にだけあるとは限らず、たとえば自分の状態を指すポインタ群を保持したベクタの中にある場合もある
    • こうしたポインタをすべて追跡するには、結局ガベージコレクションに近いランタイムメモリ管理が必要になる
    • Rustは初期段階でmove constructorを持たない方針を選んでおり、多くのunsafeコードは値をメモリコピーだけで移動できるという前提に依存している
    • 後からmove constructorを追加するとbreaking changeになる
  • offset pointer

    • offset pointerは、自己参照を通常の参照ではなく、自己参照オブジェクトのアドレス基準のオフセットとしてコンパイルする方式である
    • コンパイル時点では、ある参照が自己参照かどうかを常に判定できるわけではない
    • 分岐によっては同じ値が自分自身のオブジェクト内部を指すこともあれば、外部を指すこともある
    • これを処理するには、参照をoffsetとreferenceのenumのような形にコンパイルする必要があり、async/awaitの開発当時には現実的でない方法だと判断された

pinned typestateの要件

  • 自己参照Futureは、最初から常に移動不能である必要はなく、ライフサイクル中のある時点までは自由に移動でき、特定の時点以降だけ移動してはならない
    • Futureを別のFutureと組み合わせている間は移動可能である必要がある
    • pollされる場所に配置された後は、それ以上移動してはならない
  • Ralf Jungのモデルでは、既存の「owned」と「shared」というtypestateに加えて、自己参照Futureのための第3の状態としてpinned typestateが追加される
  • オブジェクトがpinned typestateに入ったら、二度と移動してはならない
    • より正確には、destructorを先に実行せずにそのオブジェクトのメモリを無効化してはならない
    • 実質的には、オブジェクトを新しい場所へ移してはならないという要件だとみなせる
  • ほとんどの型は自己参照を含み得ないため、pinned typestateは特別な意味を持たない
    • そのような型はpinningの制約から外れ、再び移動できるほうが望ましい
  • pinned typestateの詳細な形式モデルは、Ralf JungのA Formal Look at Pinningにまとめられている

?Move設計が失敗した理由

  • Pin以前には、Moveという新しいtraitに基づく設計が試みられていた
    • ほとんどの型はMoveを実装する
    • 自己参照を含み得る型はMoveを実装しない
    • Moveを実装しない型の値に参照を作ると、その値はpinned typestateに入り、以後移動できなくなる
  • この方式は、参照を作るタイミングとpinningへの遷移を結びつけて安全性を保証するという点で直感的だった
    • 実際にコンパイラのブランチに実装もされた
  • 根本的な限界は、将来的に自己参照になる値を一時的に参照したいが、まだpinningしたくないケースがあることだった
    • たとえば値を一時的にOptionに保存し、あとでOption::takeで取り出したいことがある
  • より大きな問題は後方互換性だった
    • Moveをauto traitにできなかった
    • mem::swapのように、mutable referenceから常に値を移動できることを前提にした安定APIがすでに存在していたためである
  • ?Moveとして追加する方式も、associated typeのために後方互換ではなかった
    • traitのassociated typeに?Trait boundを追加する位置はtrait定義側である
    • 既存traitのassociated type boundを緩和すると、そのboundに依存していたコードが壊れる可能性がある
    • IntoFutureのassociated future type、DerefMutTarget、関数の戻り値型、iterator item、index operatorの戻り値、算術operatorの戻り値など、多くの基本操作がassociated typeと結びついている
  • editionでも簡単には解決できなかった
    • 異なるeditionのcrate同士を組み合わせるには、traitインターフェースが同一に保たれている必要があるためである

Pinの設計

  • 最終的な設計では、pinned typestateをオブジェクト型の属性としてではなく、特殊なポインタが作り出す状態として表現する
  • Pinはポインタを包むwrapper typeである
    • built-inのreference typeも包める
    • Boxのようなライブラリ定義のsmart pointerも包める
  • Pinは、そのポインタが指す対象をpinned typestateに入れ、対象は以後移動してはならない
  • 変更を最小限に抑えるため、この設計はコンパイラ機能ではなくライブラリAPIとして実装された
    • pinnedオブジェクトを実際に変更しなければならないコードはunsafe APIにアクセスする必要がある
    • その際、通常のmutable referenceを通じてオブジェクトが移動しないという保証を提供しなければならない
  • ほとんどの型ではpinned状態と通常状態の間に意味のある差がないため、Unpin auto traitが追加された
    • 型が自己参照になり得ないなら、pinned pointerからunsafeなしでmutable referenceを取得できる
    • Unpinを実装したオブジェクトはPinの外へ移動しても安全である
  • pinningはpinned pointerにだけ適用されるため、通常のunpinned referenceはUnpinでない型に対しても引き続き機能する
  • 詳細は標準ドキュメントのPin型とpinモジュールにまとめられている
  • この設計の最大の利点は、既存コードを壊さずに追加できた点にある
    • swapのように参照先データを移動できるAPIにはmutable referenceが必要である
    • オブジェクトをPinでpinningすると、そのようなAPIはそのオブジェクトに対して以後呼び出せなくなる
    • pinned typestateが特殊なpinned referenceにのみ適用されるため、Rust言語全体の後方互換性保証を壊さない

Pinの使い勝手の問題

  • Pinは要件を後方互換の形で満たしたが、ユーザーが直接扱う瞬間に複雑性の崖が生じる
  • ひとつの説明として、pinnedオブジェクトを変更するにはunsafeコードが必要だという点がある
    • ただし、この問題は誇張すべきではない
    • Pin::setを使えば、pinnedオブジェクトに安全に代入できる
    • 実際にpinnedオブジェクトを変更する必要があるコードの大半は、async関数をFutureへ落とし込むコンパイラ生成コードであり、ユーザーが直接書く場面はまれである
  • Pinが条件付きで動作するから難しいという説明も、本質的な原因ではない
    • Rustには、条件に応じて異なる振る舞いをしつつも理解しやすさを保っている機能がある
    • non-lexical lifetimesは、条件分岐ごとにlifetimeが異なる地点で終わるようにした例である
  • 核心的な問題は、Pinが純粋なライブラリ型である一方、通常のreference typeは言語組み込み型であり、多様な構文サポートやsugarを受けている点にある
    • 通常の参照では自然にできたことが、pinned referenceでは失われる
    • ユーザーがコンパイラに受け入れられる参照の振る舞いを基準に作っていたmental modelが、pinned referenceでは崩れてしまう

reborrowingとPin::as_mut

  • 通常のmutable referenceである&mut TCopyを実装しないが、同じ引数として複数回渡すことができる
    • これはコンパイラがxの代わりに&mut *xを入れたかのように、reborrowingを暗黙に行うためである
  • Pin<&mut T>は通常のライブラリ型であり、Copyを実装しないため、このような利便性がない
    • Pin<&mut T>を2回以上使うと、move後に値を使ったというエラーや、さらに分かりにくいlifetimeエラーになることがある
    • 明示的にPin::as_mutを呼んでreborrowする必要がある
  • 通常のmutable referenceではdereferenceやassignment operatorで直接代入できるが、Pinではsetメソッドを覚える必要がある
    • このような特殊APIが増える理由は、Pinが言語構文サポートを持たないライブラリ型だからである

pinned projectionとDrop

  • pinned projectionとは、オブジェクトへのpinned referenceから、そのオブジェクトのフィールドへのpinned referenceを取得する問題である
    • projectionは、オブジェクトからフィールドへアクセスすることを意味する
  • 通常の参照におけるフィールドアクセスよりはるかに難しいため、pin-project-liteのようなサードパーティcrateが使われる
    • こうしたcrateでは、macroを含む複雑な新しいAPIを学ぶ必要がある
  • 最も厄介な相互作用は、pinned projectionとDrop traitの間で発生する
    • Drop::dropは通常のmutable referenceを受け取る
    • ある型が自己参照フィールドを持ち、そのフィールドにpin projectしてpollしたあと、destructorでそのフィールドを移動してしまうと、pinning保証を破り得る
    • たとえばdestructorの中でそのfutureをstack上にpinningしてpollすると、既存のpinning保証が破られる
  • pin-project-liteのようなcrateは、destructorを定義する能力を制限することでこの問題に対処している
    • 実務上は機能するが、pinning保証を説明する際に文書化すべき複雑さが増える
    • DropPinより先にstable化されていたため、回避策が必要だった

現在の評価と次の改善方向

  • Pinは、任意の参照を含むasync関数を安全な自己参照オブジェクトへコンパイルできるようにした
    • 参照はRustユーザーがコードを書くうえでの基本的な手段であり、これがなければasync/awaitの使い勝手は大きく損なわれていただろう
  • 同時にPinは、既存Rustと完全に後方互換な形で追加された
  • Pinは、高性能ネットワークサービスや非同期プログラミングのその他のユースケースを支えるエコシステムの基本構成要素となった
  • しかしpinned referenceを扱うのは通常のreferenceを扱うよりはるかに難しく、Pinは実際に複雑性の崖を作っている
  • 次の改善方向における中核概念はpinned placesである

1件のコメント

 
GN⁺ 2024-07-22
Hacker Newsのコメント
  • Pinは公式ドキュメントで明確に説明されておらず、理解しにくいとずっと思っていた
    特に「Pinはオブジェクトが絶対に移動しないことを保証する」といった説明が多いが、実際にはそうではない
    オブジェクトがUnpinでない場合にだけ正しい話で、ほとんどの普通のオブジェクトはUnpinなので、Pinは通常何もしない
    これを理解するのにとても長くかかったし、Pinが実際に意味を持つ型Tの集合はかなり特殊で奇妙なのに、ドキュメントはそこを十分に強調していないと思う

    • 良いフィードバックで、ドキュメントがこの点をもっと明確にするとよいと思う
      もちろん、実際に固定された状態として扱うことになる型であるFutureとStreamは、そうした特殊なオブジェクトである可能性がはるかに高い
      それでも、ドキュメントはここ数年でかなり良くなったと思う
      この記事を書くときに確認したら、かなり適切な点に焦点を当てていて驚いたし、2019年ごろはstd APIドキュメントというより、Rustリファレンスに入るような契約仕様にずっと偏っていたと記憶している
  • ユーザーがPinを難しいと感じる理由は、Pinそれ自体には意味がないからだと思う
    これは言語の他のラッパーとは異なり、例外を挙げるなら本来の用途ではほとんど誰も使っていないAssertUnwindSafeくらいだ
    Pin<&mut InnerType>があるとき、言語や標準ライブラリ内のPinだけでは、何ができて何ができないのかを示すものはない
    ただしInnerTypeUnpinだと宣言されていれば、通常のポインタでできることはすべてできるという意味になる
    代わりにPinは「意味は自分で持ち込め」という方式で動作し、InnerTypeの提供者が、固定されたオブジェクトを安全に操作するための内部的にはunsafeなメソッドやAPIを追加で作る構造になっている
    Pinそれ自体の目的は、&mutの置き換えやBoxから取り出して移動することのような内在的な能力がより少ないポインタを提供し、内部の型がその上に追加の能力を安全に許可できるようにすることにある
    この意味の曖昧さが人々を最も混乱させていると思うし、自分も理解するのにかなり時間がかかった
    構造的フィールドと非構造的フィールドに関する概念は、「このフィールドは普通のデータだが、あのフィールドはそれ自体が固定されることを望むオブジェクトを含んでいる」といった一般的なアクセスパターンを可能にするための仕組みにすぎない

    • Pinには意味がある。対象の型がUnpinを実装していない限り、このポインタの対象は二度と移動できないという意味だ
      より正確には、デストラクタを実行せずに対象を無効化できないという意味で、移動が問題になる理由もそこにある
      特定の権利を手放すと、自己参照値を保存する権利のような別の権利を得る
      コンポーネント間の契約はおおむねこのように動作する
      同様に、参照を通じて変更する権利を手放すと、同時にその参照をエイリアスにできる
      この点を考えるたびに、まったく別でずっと重いテーマではあるが、映画『Lincoln』の台詞「われわれが法に従うなら、Alex、自由を失うところまで従うなら――たとえば抑圧する自由を――以前には知らなかった別の自由を見つけるかもしれない」を思い出す
      ただし、安全なコードではそうした権利を直接使えないという点が、教育上の問題だということには同意する
      固定された参照で何ができるのかを、「コンパイラが作ってくれたpollメソッドを呼び出す」以外では簡単に示しにくいからだ
  • Rustで数年間プロとして開発してきたが、正直なところPinをそこまでよく理解していない
    理論は分かるが、いつ使うべきかについての直感はあまりない
    Pinの使用は実質的に「何かを試したらコンパイラに文句を言われて、あれこれ固定してみたらコンパイルできた」に近い
    日々のコーディングで、本当に腰を据えて深く理解しなければならないほどの障害になったことはまだない

    • 自分も同じ。「ただunsafeを避けて、賢いコンパイラの人たちがすでに全部解決してくれたことに感謝しよう」に当たる、最もよくあるケースの一つだ
      一方C++では、「分からないが必ず使わなければならないもの」の水辺をしょっちゅう歩いていて、ワニに食べられることがよくあった
  • 教えるときにUnpinな項目はPinの影響を受けないことを明確にするには、その場に留めるために作られた道具を使っても影響を受けない現実の比喩を使うとよいと思う
    ベルクロのフックは滑らかな表面にはくっつかない: Pin → ベルクロ、Unpin → 滑らかな表面
    磁石は非磁性物質に影響しない: Pin → 磁石、Unpin → 非磁性/ガラス/真鍮
    接着剤はノンスティック表面には付かない: Pin → 接着剤、Unpin → ノンスティック
    こうすれば、「ベルクロ」は物をその場に固定するが、物が「滑らか」ならベルクロの仕組みの影響を受けないことが明確になる
    Rustエコシステムの命名の雰囲気を考えると、トレイト名を磁石と非磁性のような方向に変えていたら美しかったかもしれない

    • しかし滑らかな物体はベルクロで留められないし、木は磁石を保持できない
      Unpinは、オブジェクトがいつでも固定される準備ができているという意味ではないかと思う
      昨夜記事を読んだのだが、固定に補正ステップが必要かどうかはもう忘れてしまった
      だからT: Pin + !Unpinはホチキスでしか固定できない紙のようなもので、T: Pin + Unpinは輪の付いた絵のように、釘に掛けてから輪を壊さずにまた下ろせるものに近いと思う
  • 「値の同一性」という用語はこの記事のどこにも定義されておらず、Mojo のドキュメントでも見つけられなかったので、Modular が Mojo は Pin が解決しようとしている問題を解決すると言う根拠がはっきりしない
    私も答えを知っていると主張するつもりはないが、Chris Lattner とともに Swift の値セマンティクスに取り組んだ Dave Abrahams のすばらしい発表を思い出す
    発表タイトルは「Value Semantics: Safety, Independence, Projection, & Future of Programming」
    [0] https://www.youtube.com/watch?v=QthAU-t3PQ4

    • Mojo がある意味では Swift の値セマンティクスの概念を受け継いでいるのは明らかだが、Rust も同じ意味での値セマンティクスを持っている
      Rust は参照もファーストクラスの型として持っている一方、Swift と、私の見る限り Mojo は、参照を引数の渡し方としてのみ許している
      Mojo は Swift の inout パラメータを拡張し、不変参照渡しの方式も備えているように見える
      オブジェクト内に参照を保存できないようにすれば、Rust がコンパイルするようなコードは実装できないので、「自己参照構造体」の問題は解決する
      しかし引用された段落が Mojo について述べている内容はまったくそういう話ではないので、何を意味しているのかかなり混乱する
  • 私には、問題はある値への &mut 参照があると、mem::swap/replace のようなものでその値を移動できてしまう点にあるように見える
    しかし実際にそうする必要があるケースはまれだ
    それが許されていなければ、自己参照値への &mut 参照を持っても完全に安全だったように思う
    必要なときだけ参照経由の移動を明示的に選択する方法があったかもしれないし、swapreplaceunsafe にしていれば、この問題全体を避けられたのかもしれない
    誰かこの設計空間を探ってほしい

    • その通り。この問題に取り組んでいた当時、Aaron Turon は &mut強力すぎると表現していた
      &mut がその中の値を移動する権限を与えていなければ、設計全体はずっと単純になっていただろう
      次の記事でこの内容を扱う予定
      Rust は後方互換性を守らなければならず、すでに &mut から値を移動できると決めてしまっているが、過去の決定に縛られないなら、もっとずっときれいな設計が可能なのは明らかだ
    • これは正しいが、スケールしない。既存コードをあまりに多く壊すため、そもそも成立しにくかっただろう
      mem::swap は可変参照を通じて値を移動する方法の一つにすぎず、ほかにも方法は非常に多い
      Option::take は私がかなりよく使う例だが、これが unsafe だとしたら本当に変だろう
  • この背景話はいい。WithoutBoats はすでに、非常に時宜を得た非同期イテレータpollpin というテーマで活発な議論を多く行ってきた
    https://news.ycombinator.com/from?site=without.boats
    言語の細かな内部をここまで深く公開の場で掘り下げるコミュニティは多くないように思うし、見ていてとても面白い

    • すばらしいことではあるが、その分、言語開発が非常に遅いという意味でもある
      async はまだ半分未完成で、非常に複雑だ
      過去 3 年間、週 40 時間 Rust コードを書いてきた立場からの発言だ
  • ムーブコンストラクタがあり、生成されるすべての Future サブタイプが不透明で、自動的にヒープに割り当てられる、Rust に似た言語を想像することはできる
    そうすれば、ユーザーにはそれを壊す方法がなく、不透明でヒープ上の別の場所にあるため移動する方法もないので、Pin が不要になる可能性がある
    ムーブコンストラクタがあるということは、移動が概念的には破棄後の再生成だという意味だからだ

    • Pin はデータそのものの属性というより状態である
      そのおかげで、実行前に Future たちを結合してインライン化できるという良い効果がある
      Rust の不変性にも似ている。不変なメモリがあるのではなく、不変参照だけがあるのだ
    • すべての Future がヒープに割り当てられるなら、ムーブコンストラクタは必要ない
      しかしそうすると、非同期関数呼び出しのたびに別個の割り当てが発生し、これはメモリ局所性にとって非常に悪い
      何らかの形の仮想スタックのほうがはるかにましだろうが、スタックを基本的に小さく最適化しようとすると、結局ガベージコレクションが必要になる
    • どれほど破壊的な変更になるかは想像できるが、Rust が真正面から受け止めて、Copy と同程度に言語へ組み込まれた Move トレイトを std に追加してくれたら本当にうれしい
      Move は値をメモリ上のあるアドレスから別のアドレスへ移動する関数を定義し、impl Move のない構造体は移動できないようにするものだ
      ほぼすべての型には #[derive(Move)] を付け、これはバイトをコピーするだけの単純な移動関数を実装すればよい
      しかしこうすれば、自己参照型、Future、そしてより複雑な移動動作を必要とする多くのものへの道が開ける
      実際には、CopyClone の違いを反映して 2 つのトレイトに分けるほうが筋が通るかもしれない
      片方はバイトをそのまま移してよいとコンパイラに伝えるマーカートレイトで、もう片方はユーザー定義の「ムーブコンストラクタ」実装を許す方式だ
      Pin は理解するのがあまりに難しいので、Move があればいいのにと思う
      複雑な概念が二重否定、時には三重否定で包まれている。fn(...) のようなものを見ると「何だこれは?」となり、unsafe な pin プロジェクションあたりまで来るとついていけなくなる
      いつ安全で、いつ安全でないのか分からず、ただ手を引くことになる
      Move のない Rust から Move のある Rust へ移行するのは不便だろう
      これまで書かれたほぼすべての構造体に #[derive(Move)] を追加しなければならず、std も同じだ
      既存のエディションで固定されていないすべての型については、コンパイラが Move トレイト実装を推論しなければならない
      機械的には可能だろうが、作業量が多いだけだ
      非同期 Rust はひどい。ほぼ他のあらゆる言語の Future/Promise と比べると特にそうだ
      いつか誰かが Rust のメモリ安全モデルを改善し、Move トレイトとより良い Future を備えた新しい Rust 風のシステム言語を作るだろう
      個人的には、Rust のマクロシステムの代わりにコンパイル時実行もあってほしい
      Rust は好きだし、チームが何年もかけて行ってきたすべての仕事も好きだ
      だが本当に期待している言語は、Rust の次に来る言語だ
      同じアイデアだが Rust の失敗から学んだ言語であり、そのようなより良い Rust 系言語がどんな姿になるのか、だんだん明確になってきている
      本当に待ち遠しい
  • WithoutBoats のまた別のすばらしい記事だ
    正直なところ、これは Rust では非同期ランタイム内部に抽象化されて埋もれているおかげで助かっている部分の一つだと感じる
    それでも、カスタム Future 実装以外でPin を実際にどこで使うのかは気になる

    • 私も気になる
      表現だけを見ると、FFI の中で使えそうにも見える
      たとえば extern 関数が *mut T を返してポインタを受け取るなら、Pin<&mut T> で包むことで、より良い意味論を与えられそうに見える
      ところが記事では「固定された型状態に関するもう一つの事実は、ほとんどの型にはまったく関係がないということだ。型の値が自己参照を絶対に含み得ないなら、それを固定することは役に立たない」と述べている
      FFI についてはまだ完全な初心者なので、安全な Rust で包む最良の方法を理解したい
    • FFI では、C API が項目を参照ではなくポインタとして公開しており、移動してはいけない場合がある
      アドレスに依存するシステム型とやり取りする場合もこれに当てはまる
      たとえば一部の OS の mutex/futex では、カーネル文書がユーザー空間のロックオブジェクトは初期化後にアドレスが変わってはならないと述べているため、std が Pin に相当するものを使っていると理解している
      ロックされていないときでもアドレスが変わってはならない点が特異だ
      通常はロックされている間だけの条件で、その場合は参照が指している対象を移動できないため Pin は必要ない
  • 本当の問題であるスレッドの非効率性を直そうとせず、膨大な作業をしているように見える
    すべての非同期コードは例外なく、状態管理のための大量の構文糖衣で軽量スレッドを実装するハックである
    Rust のような言語では、本来なくてもよい複雑さを途方もなく追加している
    スレッドの効率性とスケーラビリティの問題を直せば、これらはすべて消えるはずだ
    ぱっと消えてなくなる
    Java のような言語で null が「兆単位のコストを生んだ過ち」だったのと似ている
    1つの設計判断、あるいはここでは設計の欠如によって、膨大な複雑さが生まれている

    • スレッドは合理的な形でのキャンセルをサポートしていない
      キャンセルはネットワークアプリケーションや GUI で非常に有用である
      スレッドは CPU とネットワークの両方を十分に活用しつつ、どちらも過度に占有しないようにするのを難しくする
      スレッドプール間で作業を受け渡し始めると、それは Future を再実装する道に入ったということだ
      あるいはコールバック/イベントで処理することになるが、コードが断片化し、async/await はまさにそのための構文糖衣になろうとしていたものだ
      キャンセルとタイムアウトの代替案は、Go のように Context オブジェクトをコード全体に組み込むことだが、末端のコードが Context に正しく従わない関数を素朴に呼び出してしまう問題が生じる
      これは非同期コード内の非非同期関数の問題より、ほんの少しましなだけである
    • 「スレッドの効率性とスケーラビリティの問題を直す」ために Linux カーネルを書き直すことが可能なのかは疑わしいし、可能だとしても Pin が動くようにした Rust の専門家たちと、そのようなことができるカーネル専門家の集合はおそらく同じではない
      なので具体的に何をすべきだったというのか気になる
      ただ手を上げて「いつか誰かが Linux を直してスレッドを魔法のように速くしてくれるかもしれないので、私たちの言語には非同期を追加しません」と言うべきだったのか?
    • 並行プロセスと同期する関数と、そうでない関数を区別して示すことは、実際には良いことだ
    • 残念ながら、ユーザー空間の境界を越えるには「スレッド」がどれだけ軽量でもコストがかかる
      また、OS をすべての非同期処理のスケジューラにすると、すべてのランタイムが OS スケジューラを使わざるを得なくなるため、多様なスケジューラ設計が不可能になる
    • Rust が下した有害な判断は、過去の過ちを何が何でも押し通す文化が深く根付いていることを示している
      「好ましいアプローチ」が実行不可能だと明らかになった後でも、費用対効果を再評価しないように見える
      「機能 X が欲しい、結果は知ったことではない」という姿勢は、言語設計ではめったに勝ち筋にならない