おそらくRustは大規模な並行ユーザー空間ソフトウェアには向いていないかもしれない
(bitbashing.io)- Rustの
async/awaitは数万件の接続を扱う大規模な並行性を想定しているが、低レベル制御と静的ライフタイム検証というRustの目標と衝突し、通常のRustとは異なる開発体験を生む - スレッドとチャネルは多くのソフトウェアには十分だが、C10Kのような規模では接続ごとにスレッドを割り当てる方式の負担が大きくなり、ユーザー空間タスクとランタイムスケジューリングが必要になる
asyncRustではデータをSendとして移動させるか、'static参照として扱う必要があり、asyncの伝染性によってこうした制約がコード全体で繰り返されるArcはコンパイル上の問題を解決しても、オブジェクトやリソースのライフタイムを曖昧にし、再帰async、futureとtaskの違い、blocking呼び出しがランタイムスレッドを止める落とし穴が続く- HaskellやGoでは「async code」が通常のコードのように動作し、ランタイムとGCが違いを隠すため、この種のプログラミングではRustの明示的な制御が純粋な利点としては働かないかもしれない
並行性と並列性が必要な理由
- 高速なプログラムには2つの要求が同時にある
- 複数のCPUコアを使ってコンピュータ全体を活用しなければならない
- インターネットメッセージの送信やファイルを開くといった遅い処理を待つ間も、別の作業を続けなければならない
- 並列性は複数のCPUでコードを同時に実行する問題である
- 並行性は問題を独立した部分に分ける方法である
- 両者は同じではないが、プログラムを並行する断片に分ければ、それらの断片を並列に実行してコアを継続的に忙しくできる
プロセス、スレッド、チャネル
- 並行システムを作る単純な方法は、コードを複数のプロセスに分けることだ
- OSスケジューラが、実行可能なプロセスのタイムスライスを利用可能なCPUコアで実行する
- シェルコマンドをパイプでつなぐ場合にもこのモデルを使う
- プロセス方式はプロセス間通信のコストが大きい
- 多くの実装では、データをOSメモリにコピーしてから再び取り出さなければならない
- 共有メモリでコストを下げられるが、OSがプロセス同士を隔離してくれる利点は弱まる
- スレッドは同じメモリを共有することでこうしたオーバーヘッドを避けるが、mutex、condition variable、semaphoreのような同期手段を誤って使うと、データ競合やデッドロックが発生しうる
- Tony HoareのCommunicating Sequential Processesモデルは、スレッドをキューまたはチャネルで接続する
- スレッドはメモリを共有しないため、プロセスのような隔離性が得られる
- 各スレッドの入力と出力がチャネルとして表れるため、推論やデバッグがしやすい
- チャネル自体が同期の役割を果たし、空なら受信側が待ち、満杯なら送信側が待つ
- Rust標準ライブラリにはstd::sync::mpsc::sync_channelがある
- 多くのソフトウェアでは、スレッドとチャネル、それにCPU集約ループを並列化するRayonのようなツールの組み合わせで十分である
ユーザー空間の並行性とRust async
- Webサーバーのように数万人の同時ユーザーが接続するC10K問題では、1接続に1スレッドを割り当てる方式は限界にぶつかる
- Linuxでは各スレッドが4kBのcontrol blockを持ち、スレッド切り替えにはOSスケジューラに入るcontext switchが必要になる
- 大規模な並行性のために、一部の言語はユーザー空間でタスクを作成・管理する
- ランタイムがタスクをOSスレッドプールにスケジューリングする
- 通常はCPUコアごとに1本のスレッドが配置されるようにプールを構成し、並列性を最大化する
- この方式はgreen thread、lightweight thread、lightweight process、fiber、coroutineなどと呼ばれる
- RustはC#やNode.jsで見られる
async/awaitモデルを使っているasync fnは直接値を返さず、.awaitで結果を得るfutureまたはpromiseを返す
- Rustのfutureは協調的スケジューリングとstackless設計のおかげで非常に小さく高速である
- Rustはfuture抽象化を提供しつつ、プログラマに低レベルの制御を約束しようとする
- すべてのオブジェクトと参照のライフタイムをコンパイル時に静的に検証しようとする
- futureはコードと、そのコードが参照するデータを何千もの断片に分割し、実行開始後にしか分からない条件に応じて、いつでも任意のスレッドで実行されうるようにする
- クライアントデータを読むfutureは、そのソケットに読むべきデータがあるときにだけ実行されるべきだが、lifetime annotationはその時点を教えてくれない
- Rustはfutureランタイムを言語に組み込まず、Tokioのようなライブラリに委ねている
- ユーザーは環境に合った代替手段を選ぶ自由を得る
- しかし、Tokioが言語に組み込まれている世界を想像しても同じルールが適用されるため、この論点では副次的な詳細にすぎない
Send、'static、Arcが生む圧力
- コンパイラを納得させるには、データを
Sendとして移動させるか、'staticライフタイムを持つ参照経由で渡す必要がある asyncコードでは複数のタスクが共通状態を共有することが多いため、コピーなしでデータを移動する方式はしばしば適合しない- 参照も難しく、futureのライフタイムを「永遠」より短く制限してくれる
thread::scopeのような対応物もない asyncには伝染性があり、async関数を呼ぶ関数もasyncでなければならない- そのため、こうしたライフタイムと移動性の問題を一部の関数だけでなく、継続的に解決し続けなければならない
- ランタイムでfutureの完了を
block_onで待って連鎖を断ち切ることはできるが、この方法は合成しにくく、ネストするとランタイムがpanicすることがある
- Arcは、動的なライフタイムを複数スレッドにまたがって扱うための道具であり、borrow checkを通してコードをコンパイル可能にしてくれる
- しかし
Arcを広範に使うと、オブジェクトやリソースのライフタイムが曖昧になる- メモリ、ファイル、ソケットのようなリソースがいつ解放されるのか明確でなくなる
- 実際のGCが提供するallocation throughput、低いfragmentation、cycle leak回避といった利点なしに、GCに似た不利益を被る
async Rustの追加の落とし穴
- Rustのcoroutineはstacklessなので、コンパイラは各coroutineを
.await地点まで進める状態機械に変換する- 再帰
async関数は再帰的に定義された型になる - 単に自分自身を呼びたいだけのユーザーでも、手動でboxingするか、async-recursionのようなcrateを使わなければならない
- 再帰
- futureはawaitされるまで何もしない
- taskはランタイムのスレッドプールで作業を開始し、完了を示すfutureを返す
- futureの内部でblockingコードを呼べないように防ぐ仕組みはない
- そのような呼び出しが、自身が載っているランタイムスレッドを塞ぐことも防げない
- これは
asyncを使う主要な目的と衝突する
通常のRust、Haskell、Goの違い
asyncRustは「通常の」Rustとはかなり感触が違う- 落とし穴が多い
- 理解するのも教えるのも難しい
- ユーザーは2つの選択肢の間に置かれる
- 抽象化が実際にどう動いているかを深く理解し、複雑なコードを書く
Arc、Pin、'staticのような要素をコードのあちこちにばらまいて、うまくいくことを祈る
- 経験豊富な開発者チームでも、新規プロジェクトでRustを使おうとしてこうした詳細に足を取られることがある
- HaskellやGoでは「async code」が通常のコードである
- 両言語は、分厚いランタイムの背後にblockingコードとnon-blockingコードの違いを隠している
- ライフタイムの問題はgarbage collectionに任せている
- この種の大規模な並行ユーザー空間ソフトウェアでは、ランタイムとGCが違いを隠す方式は純粋な利点として働く
- Rustは大規模な並行ユーザー空間ソフトウェアには良い道具ではないかもしれず、そのような要件のないプロジェクトで使う方がよいかもしれない
1件のコメント
Hacker News のコメント
高性能なメタバースクライアントを Rust で書いており、現在およそ4万行規模
デモ動画は https://video.hardlimit.com/w/tp9mLAQoHaFR32YAVKVDrz にある
まともなメタバースなら、ユーザーが作ったコンテンツをほぼリアルタイムで処理する必要があるため、類似のゲームより VRAM が2〜3倍必要で、サーバーからアセットを読み込むには数百 Mbps の帯域幅と複数の CPU、レンダリングと GPU アップロードを並行して行う Vulkan が必要になる
これは「Web スケール」の並行性のように、同じアドレス空間で小さなサーバー群が別々に動く構造ではなく、高優先度のレンダースレッド、ネットワークイベント更新スレッド、アセットの読み込み・展開スレッド、移動オブジェクト・LOD・キャッシュ整理などを担当する複数のスレッドが一緒に動く構造
Rust では、定数以外のグローバル状態なしにかなりのロックを使い、チャネルは適した場所で使い、主要なオブジェクトツリーは単一所有権で更新スレッドが主に扱っている。グラフィックスオブジェクトの接続は
Arcの参照カウントで管理し、Rend3/WGPU/Vulkan を通じてメッシュとテクスチャを GPU にアップロードするC++ でやっていたらクラッシュと戦い続けていたはずだが、Rust ではメモリ関連のクラッシュは年に1回程度で、それもたいてい他人の
unsafeコードだった。自分のコードではunsafeを禁止しており、コンパイルは難しいものの、いったん通れば「そのまま動く」ことが多いので、並行性のデバッグよりはるかに良いと思っている不満もある。Rust はデータ競合には強いがデッドロックは防げないので、呼び出し経路に沿ってロック順序を追跡する静的解析器が必要だ。
asyncは計算中心の作業や複数の優先度スレッドには向いていないのに、依存関係としてしつこく入り込んでくる。単一所有権に逆参照が付くようなよくある構造はRcとWeakなしでは難しすぎるし、トレイトシステムも複雑で、オブジェクト指向が自然なアセット処理部分では重複コードが生じるグラフィックス中核のクレートもまだ完成度が低い。「Rust にはゲームが5本、ゲームエンジンが50個ある」というのは言語の問題ではなくエコシステムの問題で、https://gamedev.rs/ と比べても、Rust による本格的なゲーム開発はまだ不足しているように見える。スケジュールのあるプロのゲーム開発なら、Rust のゲームエコシステムはまだ準備ができておらず、おおよそ5人があと1年ほど取り組む必要があるレベルだと思う
asyncに問題はあるものの、全体として利点の方がはるかに大きいLinux の
lockdepのように、あるロックを保持した状態で別のロックを取るかどうかを分析し、実際に止まる前でも危険な組み合わせを知らせられる。複雑なロックでは「このロッククラスは常にアドレス順に取る」といった注釈が必要になるだろうが、実装可能に思えるデータアクセスの外側では依然として競合状態は起こり得る: https://news.ycombinator.com/item?id=23599598
複数のスレッドプールを作って future を適切にルーティングするか、自前のイベントループを書いて優先度の異なる複数のイベントキューから取り出せばよい。2つ目の方式は、タスクの実行時間に上限があるなら、CPU 100% の状況でも低優先度のタスクを進めながら高優先度のタスクにソフトリアルタイム保証を与えられる
asyncRust については微妙なところにいるArc、RwLock、共有状態を大量に使うとごちゃごちゃし、特に'staticがあちこちに広がり始めると、色付き関数のようにすべてを感染させるという指摘は正しい。以前はArcを付けてライフタイム付きの借用を賢く扱おうとして、めちゃくちゃになっただが Rust にはチャネルもある。今書いているコードの大半は、いくつかのタスクがチャネルをサービスし、入ってきたメッセージを見て、必要なら別のタスクへ送るメッセージを適切なチャネルに入れる構造だ。オブジェクト共有はしない。複数のタスクが大きなオブジェクトを必要とする場合は、関連するクエリ結果をメッセージとして送るタスクの中に置くか、各タスクがメッセージフローの中で自分用のコピーを作るようにしている
それでも
Arcをどう使うか、ライフタイムをどう扱うかを取り上げる記事が多すぎる。asyncランタイムを実装するなら必要だろうが、平均的なライブラリ利用者がなぜそこまでここに集中しなければならないのかはよく分からないasyncがただちにマルチスレッドを意味するわけではなく、同じスレッド内のasyncなら共有がないため、共有対象に魔法のようなキーワードを全部付ける必要もないスレッド間を行き来する場合は、共有状態を大量に持つよりチャネルでシグナルを送る。どうしても必要なグローバル状態があるなら、
Arc/RwLockのような排他アクセス機構を包んだ小さな構造体を作り、呼び出し側からは単純な関数呼び出しのように見えるようにするSend+Syncへの心配もよく分からない。経験上、ほとんどは簡単にSend+Syncであり、そうでないものは、そうあるべきでないか、そうなれないものだった。細部を考えずにコードを書きたいときもあるが、効率的な並行性・並列性が必要ならマイクロ秒とスループットが重要になり、そのときは実際のコンピューター向けのコードをきちんと書く必要があるしかし、このようなコーディングは、同期コードにグリーンスレッドを付けたように感じられる
asyncJavaScript とはかなり違う。人々は慣れたやり方でコードを書こうとするため、Rust でArcとRwLockの道に入り込むのだと思う問題をタスク間を流れるデータとして構造化し、キューでつなぎ、共有状態を避けるやり方は、どの言語を使うにせよマルチスレッドを扱うより良い方法だ
asyncは実質的にはるかに難しい Rust であり、実際に必要とするプロジェクトはおそらく 1% ほどのはずなのに、ほとんど全員に強制されるようになってしまったのは残念ただし、その 1% では本当に素晴らしい。linkerd や nginx のように大量のネットワーク呼び出しを中核として処理するサービス、ゲームで膨大な数の軽量タスクを動かす場合、組み込みで協調的並行性が必要な場合には、async Rust は強力な武器になる
ほとんどのシステム/アプリケーションレベルのコードには非同期 I/O は必要ない。REST アプリはスレッドプールで十分で、
asyncが必要だとしてもネットワークのような小さな部分に限定し、残りはスレッドとチャネルでつなぐ 混合モデル がたいてい適しているRust コミュニティは
asyncをあまりに無分別にあちこちで使ったため、ユーザー体験のよいブロッキング I/O の Rust がエコシステム内で二級市民になってしまった。Web フレームワークも Axum、Warp のようなよく設計された非同期フレームワークはいくつもある一方で、ブロッキング側はtiny_http、rouille、astraのように選択肢がずっと限られているスタックレスコルーチンを選んだことで
async/awaitと色付き関数問題が生まれ、記事で述べられている摩擦が生じた。Go はスタック付きコルーチンを使っているため、こうした問題はないRust も当初はスタック付きコルーチンを検討していたが、コルーチンのプリエンプティブなランタイムが必要でコストが大きいと見て、スタックレスモデルに進んだ。しかし大半の人はランタイムなしの
asyncRust を使わず Tokio を使っており、Tokio は避けようとしていたランタイムが行う仕事を実質的にほぼすべて担っているそのため、多くの
asyncRust ユーザーは両方の悪いところを抱えることになっている。組み込み方面では非常に薄いランタイムでasyncRust を使うこともあるが、その数は少なく、彼らでさえ完全に納得しているわけではないreqwestを取り込み、それがh2を取り込み、さらにtokioを取り込むasyncを使う理由があるのか気になるJava を使う立場としては、非同期パラダイム全体を捨てて、ブロッキングで問題ない仮想スレッド上のブロッキングモデルへコードを書き直そうとしている
asyncがあまりに多くのクレートに広がっているため、プログラム全体がasyncになるか、少なくとも多くのことを Tokio に依存するようになるWeb サーバーが欲しければ
async + tokioでなければお断り、という感じで、SQL コネクタも非同期を望まないなら自分で書くしかないような雰囲気だ。それぞれがasyncのもたらす問題を別々の方法で解決していて、asyncクロージャのようなものはコンパイラに地獄の門を開くように感じるRust 自体とコンパイラが問題解決を助けてくれる点はよいが、エコシステムが「
asyncでなければ自作しろ」に近いのは十分ではないfuturesクレートに、よりよい 非同期プリミティブ があれば、苦痛をかなり減らせたはずだエグゼキュータが実装すべきトレイトや、同期コードから非同期コードを実行するための基本的なブロッキングエグゼキュータのようなものが必要だ。今は複数の非同期ランタイムをサポートするライブラリを作るだけでも苦行なので、結局 Tokio だけをサポートするか、せいぜい
async-stdを追加する程度になってしまうasyncRust の専門家ではないが、今月同期 Rust を数千行書いて感じたのは、rustcがあるアプローチを難しくしているときは、たいていそれなりの理由があり、似た結果をよりよい方法で得る道があるということだ言語を学んでいる途中なら、まずは普通の 同期コード、ループと条件分岐、借用規則に慣れることを勧める。
asyncは実装だけでなく、「非同期とは何で、ユーザーにどう見えるべきか」という哲学的なレベルでも、まだ大きく発展中だコンパイラはトレイトに大きく依存しているが、トレイトが
asyncを扱う機能は安定化されていない。たとえば https://blog.rust-lang.org/inside-rust/2022/11/17/async-fn-i... のような取り組みがあるトレイトの非同期機能が安定化されていないのなら、Rust の非同期コードがまだきれいではないと攻撃するのは、結局、いずれ完成する本の初期草稿を批判するのに近い
非同期がコードベース全体へ広がるのも、どう防げるのか悩ましい
現在の構想は、I/O スレッドが
liburingやepollのシステムイベントを「submit」と「handle」の 2 段階に分けて別のコンポーネントへ送る構造だ。たとえばtcp-connectionを作ると、「書き込み可能」「読み取り可能」のような非同期イベントを購読でき、書き込み可能イベントは通常の mutex で満たされたバッファからデータを取り出してEPOLLOUT/io_uring_prep_writevで送るスレッド間のイベント伝達には、LMAX Disruptor パターンの複数生産者・複数消費者リングバッファを使える。アプリケーションスレッドやスレッドプールは、それぞれイベントループを持ってこのリングバッファを処理する
非同期イベントの発火順序を表現する構文にも取り組んでいて、Bash パイプラインのような見た目で
statelinesと呼んでいる:initialstate1 initialstate2 = state1 | {state1a state1b state1c} {state2a state2b state2d} | state3Arcの寿命は不明なのではなく、どこでどのように保持されるかで決まるこの記事とのずれは、筆者が Rust を学んで言語に合わせて作業するというより、ガベージコレクションのような以前の思考モデルを Rust に無理に当てはめようとしているところから来ているように思える。新しい言語を学ぶときによくある落とし穴だが、Rust では特にさらに頻繁につまずきやすい
しかしそれは、コンパイル時にオブジェクト寿命を静的に制限しようとする借用チェッカーの目標とはほぼ正反対である
実際にはほぼ逆だった。C、C++、Rust でシステムプログラミングを約10年やった後、現在の職場で Haskell を多用するようになり、大きな言語ランタイムとガベージコレクションが、ある問題領域では怪物ではないという点にかなり目を開かされた
async変換が、非同期ではないコードでコンパイラができる最適化をどのように妨げるのかについての記事だと思っていたWeakと格闘しているというくだりは、複雑な所有権構造を作ろうとしているように見えるが、これは Rust 全般で簡単なことではない。私は弱いスマートポインタを非常にまれにしか使わないチャネルはほとんど言及されていないが、非同期コードや非同期・同期コードの間をつなぐときに、プログラムの異なる部分を通信させる主な道具である。
Notifyやセマフォのようなシグナル抽象化もあるmutex は遅く、ボトルネックになりやすく、共有状態はすぐに複雑になる。これは昔から知られている事実だ。問題はそもそも
BIG_GLOBAL_STATIC_REF_OR_SIMILAR_HORRORのような構造にあるのかもしれない非同期コンテキストでブロッキングコードを呼ぶことを防げないという指摘は妥当だが、必要なら
tokio::spawn_blockingのようなもので比較的管理可能である筆者は
Arcが何であり、どのように動作するかを知っている可能性が高く、要点は Rust のasyncでは同期コードよりも通常の RAII の代わりにArcをはるかに頻繁に使うことになる、という点に近いプログラムオブジェクトの90%が参照カウントされるなら、多数の小さなヒープ割り当て・解放とアトミック操作のコストを払うより、トレーシングガベージコレクションを使うほうがよいかもしれない。Tokio のチュートリアル例も似た方向を示している: https://tokio.rs/tokio/tutorial/shared-state
Rust で実際のトレーシングガベージコレクションが、HTTP サーバーのような一般的な非同期アプリケーションを意味のある形で高速化できるのか気になる: https://manishearth.github.io/blog/2015/09/01/designing-a-gc...
Arcの寿命はランダムなのではなく、静的には分からないのであるArcは移動したり借用したりでき、参照カウントに触れずに使うこともできる多くの場合、暗黙的な参照カウント言語のオブジェクトよりはるかに安価である
Rust は好きだが、
asyncはめちゃくちゃで、同期コードを書くように非同期コードを書くことはできない両者を混ぜるのは悪い考えだという確信が次第に強まっており、すべてを同期式のままにして
asyncチャネルプリミティブだけを提供する Go 的なアプローチが正しいのかもしれない今、
Futureを実装する構造体から同期メソッドを呼び出すようにロジックを配線しているところだが、かなり興味深い挑戦である。ゼロコストの非同期抽象化をユーザーにとってある程度簡単にすることはできても、その苦痛はライブラリ開発者が引き受けることになるasyncはエンドユーザーにとっても間違いなくつらく、Rust の中核機能である寿命と明示的な型が抜けた別言語を使っているような感じで、そこにPinが大量に振りかけられているように思えるスコープ付きのファイバーを実行できないため、結局
Arcを大量に付けることになり、Pinはunsafeなしでは使いにくく、非同期関数のごく小さな変更がコードベース全体の future を!Sendにしてしまうことがあるasyncの難しさの大半を解決できそうだ。今後どう発展するか見守る必要があるAsync Everything は悪い言語である
async/awaitは、JavaScript にまともなブロッキングスレッドがない問題を直そうとしたひどいアイデアで、今ではあらゆる言語に継ぎ足されている。言語とライブラリエコシステムを二分し、今後も長く苦痛を生み続けるだろうJavaScript の外でマルチスレッドをやったことがある人なら、アクターや通信する逐次プロセスがマルチスレッドに最適な方式だと知っている
Joe Armstrong の論文でも、マルチスレッドプログラムを理解する唯一の方法は、各スレッドごとに厳密に逐次的なコードを書き、複数スレッドのコードを一か所に混ぜ込まないことだと説明している。問題領域における実際の並行活動の一つが、プログラミング言語上の並行プロセス一つに正確に対応してこそ、概念上のギャップは最小になる: https://erlang.org/download/armstrong_thesis_2003.pdf
Java の Project Loom を実装した Ron Pressler による
async/await批判もよい: https://www.youtube.com/watch?v=oNnITaBseYQ彼は JavaScript 自体についてはかなり両義的で、主な目的は
epoll()の入出力イベントループを、目を突きたくならずに扱える抽象化を見つけることだった。その前にも多くの別方式を試していたasync/awaitは実際には JavaScript ではなく C# から始まったC# の Anders Hejlsberg は TypeScript も作り、TypeScript のクラス、アロー関数、
async/awaitといった機能が結局 ES6+ に取り込まれた単一スレッドのイベントループである JS/TS では優れた解決策だったと思う。しかし言語が低レベルになるほど抽象化としては悪くなるため、ここで述べられている
asyncRust 批判の大半は妥当だ記事は
asyncRust の複雑さと難しさをよく説明しているが、Rust の中核的な哲学の一つが 性能を犠牲にしないメモリ安全性 である点も重要だRust の非同期パターン、とくにコンパイラがデータ安全性を保証できるようにする仕組みは、この哲学をよく示している。複雑さはあるものの、開発者にデータと実行フローを深く考えさせる、より安全な並行性モデルとしての価値がある
すべての大規模な並行ユーザー空間アプリケーションにとって Rust が正解とは限らないが、堅牢性と安全性が最優先のシステムでは、このトレードオフは正当化され得る。エコシステムが発展するにつれて、こうした苦痛を減らす抽象化やライブラリもさらに出てくる可能性が高い
asyncベースのロックフリー Rust を大量に書いている。主な問題は Tokio の future が'staticである点で、これは Rust エコシステムの奥深くに埋め込まれた設計ミス、つまり メモリリークは安全である という決定に由来するこのため、future が適切に片付けられることを静的に保証できない。ある非同期タスクを生成したとき、誰かが
std::mem::forgetで future を忘れてしまうと、その future が推移的に渡した参照がまだ生きていることを借用チェッカーは認識できないArcをあちこちにばらまくよりは、こうしたunsafeクレートを使っている: https://docs.rs/async-scoped/latest/async_scoped/これで C++ なら作っていたであろうバグの 99% は捕まえてくれるので、妥当なトレードオフだ。安全な方式で非
\'staticfuture を実装する作業も進行中なので、成功してほしいもう一つの大きな問題は、
async traitが現状ではボックス化された future を要求するため、関数呼び出し境界ごとにmalloc/freeが追加されることだが、これは今年の修正ロードマップに入っている「単にチャネルを使え」という助言も、大きなコードベースでは制御フローをあちこちに散らしてしまう。チャネルは現代版の
GOTOのように感じるし、自分も使いはするが、いくつかの処理を並列に実行して完了を待つだけの場合にはあまり使わない'staticなのではなく、ランタイムの並行性を活用してspawnできる future が'staticのものだけだという点であるfuture は
poll()されるにはPinされていなければならず、PinされたT: !Unpinは最終的にDropを呼ばなければならない: https://doc.rust-lang.org/std/pin/#drop-guaranteeコンパイラの
async機能で生成された future はこうした性質を持ち、手書きの future にもPhantomPinnedを入れられる。これにより、poll()された後のmem::forgetのいたずらは未定義動作だと仮定でき、侵入型・自己参照 future ライブラリも可能になる: https://docs.rs/futures-intrusive/latest/futures_intrusive/future が
Arc/Rcによって生き残り続けてリークすることはあり得るが、ライブラリ開発者の立場では、通常の使用と合理的に区別できないか、あまり気にする必要がないRcをなくすか、あるいは感染性のあるunsafeなトレイト境界を付けるほうを好むのか気になる