1 ポイント 投稿者 GN⁺ 2023-10-08 | 1件のコメント | WhatsAppで共有
  • Rust asyncランタイムをめぐる議論は、「コアごとのOSスレッド」かどうかよりも、**ワークスティーリング(work-stealing)**で負荷を均すのか、share-nothingでデータ移動を減らすのかという選択に近い
  • async Rustはすでに、ユーザー空間での並行処理と非同期I/Oというthread-per-coreの中核条件を満たしており、残る論点はコア間での状態共有をどこまで避けられるかにある
  • ワークスティーリングは、スレッドの遊休時間を減らしてテールレイテンシ(tail latency)とCPU使用率の改善を目指すが、Rustではyield地点をまたぐ状態がスレッドセーフである必要があるため、Send制約が伴う
  • share-nothingは、データを特定コアのキャッシュにとどめてレイテンシを下げられるが、キーバリューストアのように状態分割が容易な場合と、トランザクションや原子的変更が必要な場合とでは難易度が大きく異なる
  • Enberg論文はshare-nothingとshared-stateを比較しているが、どちらもワークスティーリングを使っていないため、その結果をRustのワークスティーリング実行器への批判にそのまま結び付けるのは難しい

Rust asyncランタイム論争の核心

  • Rustコミュニティでは、tokioのような主要asyncランタイムがデフォルトでマルチスレッド実行器を使い、複数タスクをワークスティーリングで動的に均衡させる選択をめぐって議論が続いている
  • 一部のユーザーは、このデフォルトのせいでSend + 'staticまたはSend + Sync + 'staticの制約がコード作成体験を損ねると批判している
  • 単一スレッドサーバーを好む場合もあるが、ここではRustシステムが1つ以上のCPUコア時間を使おうとする状況を前提とする
  • 代案として挙げられる「thread-per-core」は、より高速で実装も容易だと期待されるが、性能と実装容易性を同時に得るのは難しい

「thread-per-core」という名前が生む混乱

  • 従来のマルチスレッドasync実行器も、広い意味ではthread-per-coreに当てはまる
    • コアごとにOSスレッドを作り
    • コア数を大きく上回るタスクをそのスレッド群の上でスケジューリングする
  • Pekka Enbergはthread-per-coreを3つのアイデアの組み合わせとして整理している
    • 高コストなカーネルスレッドの代わりにユーザー空間で並行性を処理する
    • コアごとのスレッドがブロックされないよう非同期I/Oを使う
    • 同期コストとCPUキャッシュ間のデータ移動をなくすため、データをCPUコア間で分割する
  • Enbergは、高スループットのシステムを作るには前の2点が重要だが、3点目は非常に大きなマルチコアマシンでのみ必要になる可能性があると見ている
  • async Rustを使うなら、ユーザー空間での並行性と非同期I/Oという前の2条件はすでに満たされている
  • したがって議論の中心はthread-per-coreそのものではなく、ワークスティーリングshare-nothingのどちらの最適化を選ぶかにある

ワークスティーリングの目的とコスト

  • ワークスティーリングは、すべてのスレッドが継続して処理すべき仕事を持てるようにして、テールレイテンシを下げるための最適化である
  • 実際のシステムでは、タスクごとに必要な作業量が異なる
    • あるHTTPリクエストは、別のリクエストよりはるかに多くの処理を要求することがある
    • 初期段階でスレッドごとの仕事を均等に分けても、予測不能なタスク差により時間がたつと作業量に偏りが生じうる
  • 最大負荷時には、あるスレッドは処理可能量を超える仕事を抱え、別のスレッドは遊休状態のままになることがある
  • tokio、async-std、smolはいずれも、テールレイテンシ低減とCPU使用率改善を目的としてワークスティーリングを実装している
  • コストは、タスクがあるスレッドで停止した後、別のスレッドで再開されうる点にある
    • yield地点をまたいで使われる状態はスレッドセーフでなければならない
    • Rust APIでは、futureがSendでなければならないという形で表れる
    • システム状態を明確に把握できていないと、どのようにSendを保証するか判断しづらい
  • 状態が別スレッドへ移動すると、同期コストやキャッシュミスが発生し、各CPUが自分の状態だけを扱うshare-nothing原則と衝突する

share-nothingの性能ロジック

  • share-nothingは、データを複数コアで共有される遅いキャッシュではなく、特定CPUコアのより高速なキャッシュに保持することでテールレイテンシを下げようとする設計である
  • Enbergの論文 The Impact of Thread-Per-Core Architecture on Application Tail Latency は、share-nothing型キーバリューストアとshared-state構造のmemcachedをベンチマークし、テールレイテンシの改善を示している
  • Rustコミュニティでこの論文が「71%性能改善」のように単純引用されるやり方は浅く、あまり有益ではない
  • Enbergのキーバリューストアは、share-nothing構造のために状態と接続を分割している
    • ハッシュ関数でkeyspaceを複数スレッドに分割する
    • SO_REUSEPORTで入ってくるTCP接続をスレッド間に分配する
    • 接続を管理するスレッドから、該当keyspaceを管理するスレッドへ、メッセージ受け渡しチャネルでリクエストをルーティングする
  • memcachedでは、すべてのスレッドが分割されたkeyspaceの所有権を共有し、各パーティションはmutexで保護される
  • Enbergの結果は、mutexよりチャネルを使う構造のほうが、より低いテールレイテンシを達成できることを示している
    • 同じパーティションに繰り返しアクセスされ、1つのコアのキャッシュにとどまることで、キャッシュミスが減っていると考えられる
  • ただし、高度なカーネル機能と綿密に計画された構造によってデータ移動を避ける設計が、データをmutexで包む方式より実装しやすいとは言いがたい

状態分割が容易な場合と難しい場合

  • キーバリューストアは、アプリケーション状態を複数スレッドに分けやすく、share-nothingに適した例である
  • より複雑なアプリケーションで、複数パーティションの状態をトランザクション的または原子的に変更する必要があるなら、正しく実装するためにははるかに多くの注意が必要になる
  • share-nothing擁護には、過去のeventual consistencyデータベースをめぐる過熱と似た面がある
    • 性能は向上しうる
    • その一方で、データ不整合から生じるバグを避けるには慎重な設計が必要になる

Enberg論文とRustワークスティーリング論争の距離

  • Enbergの実装とmemcachedは、どちらもワークスティーリングを使っていない
  • このため、論文の主要な性能結果をRustのワークスティーリングアーキテクチャと直接結び付けるのは難しい
  • Enbergの構造にワークスティーリングを加えると、データ移動はある程度増えるかもしれないが、CPU使用率を高める方向に働く可能性もある
  • memcachedにワークスティーリングを追加する場合は、役に立たない理由を想像しにくいと見られる
  • 論文実装は、keyspaceの均等分割とSO_REUSEPORTによって、仕事をあらかじめ均等に配分するよう設計されている
  • 実環境では動的な不均衡が現れうる
    • hot keyはより多くの読み書きを受け、そのkeyspaceを管理するスレッドの作業量を増やす
    • 一部の接続は他の接続より多くのリクエストを処理し、その接続を管理するスレッドの作業量を増やす
  • 論文のベンチマークは、各接続がランダムなキーに対して一定量の作業を行うため、こうした不均衡条件を再現していないと理解される
  • hot keyを追加パーティションにキャッシュする方法のように、share-nothingシステムの中でも不均衡を緩和する設計は可能である
  • 一部タスクは状態移動を避けるため特定コアに固定しつつ、別種のワークスティーリングを最適化として使うこともできる

実務的な結論

  • CPUキャッシュ間のデータ移動を避けるようシステムを慎重に設計すれば、そうしない場合より高い性能を得られることがある
  • しかし、Send制約をgenericsに追加することを最大の不満としている程度なら、その種の細かな性能エンジニアリングをしているとは言いがたい
  • どうせshared stateを使うシステムであれば、負荷時にワークスティーリングがCPU使用率を改善しないと考えるのは難しい

1件のコメント

 
GN⁺ 2023-10-08
Hacker News の意見
  • 個人的には、この記事は大局を見失って細部にこだわっているように感じる
    議論の核心は、コアごとのスレッドのワークスティーリング実行器ではなく、Rust において async/await がそのための良い抽象化なのかどうかだ
    async コードを多用するほど抽象化が漏れ、相手にしてプログラミングするのが難しいと感じる
    人々が望んでいる代替の並行性モデルは、ワークスティーリング実行器の上にあるスタック付きコルーチンと、チャネルによる構造化並行性だ
    誰かがそれを実装して async/await と future ベースのモデルと比較するまでは生産的な議論は難しく、async が嫌いな人は避けるだろうし、Send + Sync + 'static を気にしない人は使い続けるだろう

    • それなら、本当に C10M 規模が必要な場合でなければ、単に接続ごとにスレッドを使って、もっと複雑なものを使わなければいいのではないかと思う
      その規模が必要な仕事は、たいていの場合かなりまれだと思う
      こういう記事は「カーネルスレッドは高コストだ」とだけ言って、それが本質的に真であるかのように流しているが、実際にはそうではない
      処理が何もしないタスクばかりを延々と生成するのでなければ、「本物のスレッド」のオーバーヘッドは小さい可能性が高く、その代わりに得られる単純さは非常に大きい
    • async/await 対スタック付きコルーチンの議論のほうが興味深いとは思うが、この記事の議論はそれではない
      withoutboats が扱った引用は、リンク先の記事でデフォルトのマルチスレッドとワークスティーリングを具体的に批判している部分だ
      [1] https://www.reddit.com/r/rust/comments/16p47f1/the_state_of_...
    • 世の中には、人々の意見が一致しないテーマがいくつもある
      この記事は、私が書いてほしかった議論ではなく、別の議論を扱っているだけだ
    • async/await をチャネルと併用しつつ、async 関数の引数には不変参照やコピー型だけを渡し、変更可能な共有型はチャネルで通信するように制限できる
      変更可能な型を所有してチャネルで通信するErlang 風サーバーを作ることもできるし、Arc で押し通すこともできる
      Rust はその両方を可能にする力を与えてくれる
    • 私の理解では、Rust の既存の async サポートの上でチャネルを使うことを妨げるものはない
      スタック付きコルーチンは、その段階なら単に別スレッドを使えばよいので、あまり意味がないように見える
  • 約15年前、コアごとのスレッドモデルが最初に解決しようとしていた本来の問題は、汎用マルチコアサーバーで計算のスケーラビリティと効率を得ることだった
    一部の主張とは異なり、コアごとのスレッドは明示的に CPU 中心のワークロードを最適化するためのものであり、後により洗練された I/O 処理が必要にはなったが、高スループットの I/O 中心ワークロードにも優れていることが分かった
    こうした記事を見ると、コアごとのスレッドアーキテクチャが導入されたときにあったさまざまなソフトウェア設計上の失敗を、急速に繰り返しているように見える
    公平に言えば、コアごとのスレッドに関するコンピューターサイエンスは主に HPC に由来しており、文書化が不十分だ
    この記事は、コアごとのスレッドアーキテクチャの難題である、コア間の作業バランスに焦点を当てている
    基本モデルはデータ/負荷の push/pull の4つで、ワークスティーリングは本質的に**負荷プル(load pull)モデルだ
    この方式は、ほとんど使う必要がないとき、つまり実際の問題ではまれな自然な均衡があるときだけオーバーヘッドが低い
    コア間の動的な負荷の偏りがよくある、より興味深いワークロードでは、調整オーバーヘッドのためワークスティーリングが性能上のボトルネックになる
    それでも理解しやすいので、適したワークロードでは今でも使われるが、一般化はうまくいかない
    記事で触れられていないまれなワークロードの中には、おそらく最善となる場合もある
    最近もっとも支持を集めているモデルは
    データプッシュ(data push)**のように見える。直感的ではないが、スレッド間の調整がはるかに少なくて済む
    このモデルにも適さないワークロードはあるが、一般的なほとんどのワークロードにはうまく一般化できる
    コアごとのスレッドアーキテクチャは残り続けるだろう。スケーラビリティと効率では勝てない
    ただし、現代的で慣用的なコアごとのスレッド設計がどのようなものかについて、ほとんどのソフトウェアエンジニアには直感が足りず、このテーマを深く扱う記事や論文が少ないことが状況をさらに悪くしている

    • Rust async フレームワークがワークスティーリングを使う主な理由は、フレームワークのレベルで有効にしやすく、多くのアプリケーション、特に理想的に設計されていないアプリケーションで性能を改善するからのように見える
      プロフィールの自己紹介とこの説明を見ると、そうしたアプリケーションが主に扱っている種類ではなさそうだ
      関連文献へのリンクがあれば見てみたい
    • データプッシュというアイデアが何なのか気になる
  • 「I/O 中心というのは、実際に Rust で書いたときに単一コアを飽和させるほど作業が多くないという意味だ。なら当然、単一スレッドのシステムを書け」という文には共感する
    私が使っているアプリケーションの多くは、バックグラウンドでイベントに反応するデーモンで、単一スレッドにすれば ArcMutex のオーバーヘッドをなくせる
    このオーバーヘッドはその時点ではほとんど構文上の負担に近いが、デバッグと保守は楽になる
    必要なものにだけコストを払うという点が、Rust で気に入っているところだ
    この記事が応答している元の記事は、tokio や他の async ライブラリが単純な 単一スレッドアーキテクチャに戻りにくくしていると批判している
    誇張はあるが、おおむねその批判に同意する
    高スループットのアプリケーションにより適しているという理由でデフォルトをより複雑にするのは、Rust の理想とは逆のように見える

    • そういうサービスを書いたことはあるが、それを I/O 中心とは呼ばなかったと思う
      スループットに縛られているのではなく、ほとんどアイドル状態でいて、作業が発生したらシステムリソースの使用を減らすために素早く終わらせる、という形だ
      ときどき巨大な作業のスパイクが発生し、そのときレイテンシが非常に重要でない限り、スレッドを 1 つより多く使うのは、利点なしに複雑さとオーバーヘッドを増やすだけだ
    • その通り。同時性が欲しいものすべてが Web サーバーではない
      オペレーティングシステムでは、すべてのシステムサービスが IPC リクエストを同時に処理する必要があるが、大半は全体の CPU 消費を減らすために単一スレッドで処理する
      4 コアのデバイスで数十個のサービスをコアごとのスレッドにすると、CPU と RAM の無駄遣いだ
    • tokio は本当に必要なときに使える 単一スレッド実行器をサポートしており、難しくもない
      tokio API では LocalSet と呼ばれている
      https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html...
    • 同じ引用について書こうとしていたところだった
      単一 CPU の単一スレッドで十分でも、それでも 同時性が欲しいことはある
    • ArcMutex の代わりに RcRefCell を使うことになるだろうが、コード面では同じくらい複雑で冗長ではないかと思う
      効率が低いのは理解しているが、説明されたケースなら、いくつかのアトミック操作の追加コストはどのみち無視できる程度ではないだろうか
  • 引用について言うと、「Rust async プログラミングの原罪はデフォルトをマルチスレッドにしたことだ… Send + 'static、さらに悪ければ Send + Sync + 'static が Rust を使う楽しさを殺している」という表現は、あまりにメロドラマ調だという点には同意する
    Send + Sync をなくしても、そこまで大きな違いが出るとは思わない
    いちばん気になるのは 'static で、それはワークスティーリングのせいではない
    欲しいのはスコープ付きの同時性だ
    たとえば <https://github.com/tokio-rs/tokio/issues/2596> のようなものだ
    今の Rust async でさらに本当に嫌なのは 計測の不足
    会社では一部のタスクがただ止まってしまう本番環境の問題があり、gdb; thread apply all bt に相当することをしたい
    少なくとも <https://github.com/tokio-rs/tokio/issues/5638> が入ることを期待している
    現在も実験的には存在するが、私の経験ではときどきパニックする
    今日、実際に PR を書いて、SIGTERM 時に実験版を使ってみるつもりだ
    どうせ終了中なので、クラッシュしても構わないという判断だ
    これらの不満のどれも、ワークスティーリングをなくしても解決しない
    さらに列挙しても、ワークスティーリングの削除はほとんど何の助けにもならないだろう

    • Java は多くの批判を受けているが、この部分は 25 年ほど前から普通にうまくできていて、問題解決に非常に役立っていた
      デバッガも不要で、JVM に SIGQUIT を送ると全スレッドのスタックトレースを stderr にダンプして、そのまま実行を続ける
      各スレッドがどのロックを保持しているか、あるいは待っているかも含まれる
      他の言語を使うたびにこの機能が恋しくなる
      本番環境での一時的なプロファイリングにも使える
      スナップショットを何度か取り、その後 grep/sed/sort/uniq でホットスポットを探せばよい
    • こうした問題を緩和するために、会社ではどのタスクが実行中で、各タスクが現在どのファイル/行番号にいるのかを定期的にログ出力してくれるライブラリを使っている
      すべての await 地点の前に r.set_location(file!(), line!()); を手動で散りばめる必要があるが、システムがなぜ止まっているように見えるのかを説明するのに何度も役立った
      [1] https://github.com/antialize/tokio-tasks/blob/main/src/run_t... には set_location() があり、task.rs には list_tasks() がある
  • この問題に正解はなく、すべてはユースケース次第
    結局のところ、I/O 中心か CPU 中心かというワークロード、そしてキャッシュの追い出しやロック競合のようなものがどれほど悪影響を及ぼすかの問題である
    外部データベースと通信し、その間に軽いビジネスロジックがある HTTP サーバーを共有仮想サーバー上に載せるなら、ワークスティーリングとスレッド再利用は直感的には理にかなっている
    もちろん常にベンチマークすべきである
    逆に、データベースやそれに近いシステムを作っていて、負荷時に高い並行性と多数のコンテキストスイッチがあちこちでキャッシュの追い出しや競合を引き起こすなら、苦労することになる
    その場合はコアあたり 1 スレッドが非常に妥当で、async フレームワーク自体が意味を持たないことすらある
    何が「より良い」かについて独断的な正解はない
    アプリケーションをプロファイルすべきである
    前にも言ったが、Rust 全体の焦点がWeb サービス開発の大規模な流入によって歪められているように感じる
    Rust がそうした作業に適した言語なのかはまだ確信していないが、彼らにはかなりうまく機能しているようなので、それはそれとして受け止めている
    ただし、言語に関する公開の議論や前面に押し出されるクレートは、今や全般的にこの偏りを反映している
    このフォーラムのソフトウェアエンジニアの多くが持つ偏りでもある

    • async が Rust の他の部分に影響している問題はあまり見ていない
      ゲームエンジン、暗号化ライブラリ、カーネル、コマンドラインツール、コンパイラなどのシステムアプリケーションは、async に触れなくても問題なく作られている
      大規模な暗号化ライブラリを保守しているが、async 周りのことにはまったく影響を受けていない
    • Web 開発者の流入には同意する
      その結果として Rust のシステムプログラミングにおける有用性が損なわれるなら残念だ
    • Web アプリはむしろコアあたり 1 スレッドに理想的だと思っていた
      アプリケーション自体はリクエスト外の状態がほとんどなく、ソケットリスナーとデータベース接続はスレッドごとに分けられる
      残る状態もリクエスト間では大半が静的である可能性が高く、キャッシュの無効化は頻繁には起きない
      共有状態がないので、所有権の扱いも容易なはずだ
    • Rust コミュニティが市場と認知度を得るために、そうした開発者たちをかなり積極的に引き込んだためにこうなったのだと思う
      それが良い悪いと言いたいわけではないが、今や Rust は品質がまちまちな Web 関連ライブラリやフレームワークの終わりない流れと付き合っていかなければならない
      そして中核的な基盤ライブラリやクレートのかなりの部分がasync ファーストのアプローチを採ったため、async は引き続き議論の対象であり続ける
      今では一般的な開発者がプロジェクトの主要方針として async を使わないと明言しない限り、ビジネス上の問題のために普通の同期コードを書くことが難しいレベルになっている
  • エグゼキュータのスレッド間でタスクの移動を許可するために Send 境界を要求するのは、Rust の async システム自体の明確な欠陥だと思う
    async Drop の根本問題とあわせて、スコープ付き API の実装を妨げるからである
    スレッドと同じように、spawn やチャネル経由のデータ送信のような関数にだけ Send 境界があれば十分なはずだ
    共有しないアプローチはたいてい、この欠陥を隠すための回避策以上のものではない
    タスクを特定のスレッド/コアに選択的に固定することには利点があり、状況によっては本当に有用だが、それはより細かな議論であり、Send に関する async ユーザーの不満とはあまり関係がない

  • 良い記事で、タイトルだけを見ずにもっと読んでみることを勧める
    いちばん気に入った文は「最大の不満が何かのジェネリックに Send 境界を追加することだというなら、その人がそのレベルのエンジニアリングをしているとは信じがたい」だった
    編集: 「duped」のコメントに全面的に同意する
    この議論のより大きな文脈を知らなかったため、この文をあまりに早まって引用してしまったかもしれない

  • 「システムの状態をきちんと見られない人は、future が Send になるよう保証する最善の方法を見つけるのが難しいかもしれない」というくだりは、少し傲慢に聞こえる気がする
    'static ライフタイムと Send/Sync 制約の問題は開発者に広く実感されており、彼らが愚かだという印象は受けなかった

    • 先に言ったことを改めて指している
      人々はワークスティーリングをしない方が簡単で速いと言う
      私の主張は、そのどちらかだということだ
      共有しない方式がより速くなるには、共有状態アーキテクチャをスレッドセーフにするより簡単ではない方法でコードを設計しなければならない
      次の段落には「遅い」に対応する並行した文がある
      並列・並行 Rust をコンパイルが通るようにするのに苦労している人々が愚かだとは思っていない
      ただし、私たちが用意した API が人生を台無しにしたかのように振る舞うのは好まない
    • 「難しい」が引用符で囲まれているのは、必ずしもより難しいわけではないからだ
      どうせやらなければならないことなら、より難しいわけではない
      「人々が些細な作業に過剰反応している」というより、async がいつか解くべき問題を早めに解かせるということだ
      借用チェッカーに似た感覚である
      ときには過度に制限的だが、ときにはすべて問題ないと仮定したときには考慮していなかったコーナーケースが実際に存在していた、ということでもある
    • 個人的にも、自分の状態が Send かどうかを理解するのに、ときどき確かに苦労する
      だから引用された文には共感する
  • これは大局を見落として細部にこだわっている記事だ
    すべてのプログラムで最高の性能を得る、常に正しい方法などない
    いくらでも議論はできるが、コアあたり 1 スレッドの長所と短所は典型的な「状況による」ものだ
    問題はそもそも async の使用自体が早すぎる最適化であることにある
    Rust プログラムの 99% は redis や linkerd のようなものではない
    Python や Ruby で書いても十分に速い CLI ツールや Web アプリである
    それなのに、なぜコミュニティはブロッキング I/O の Rust を捨て、すべてが async になり、開発者はデフォルトのようにあらゆるものに #[tokio::main] を付けているのかと思う

    • 1 つの理由は、コアあたり 1 スレッドの性能で十分なら、そもそも Rust を使いたくない可能性が高いからかもしれない
      速度を出す代わりにプログラミング体験がより良い言語があり、Python がその例である
      Rust を使いたいなら、おそらく追加の性能が必要なのだろうし、すでに扱いにくい言語を受け入れている以上、より良い性能のために扱いにくいスタイルも受け入れられるだろう
  • Rustを使ったことはありませんが、不満は理解できます。
    必要でもなく、CPUに十分な余裕がある状況で、単一リクエストのエンドツーエンドのレイテンシをむしろ遅くしかねない負荷の再分配のために、状態をスレッド間で移動できるようコードを特別な書き方にしなければならないなら、いら立つだろうと思います。
    移動可能な状態がデフォルトで、ほとんど破綻しないプラットフォームなら、このアプローチにも意味があるかもしれませんが、Rustではそうではないようです。
    気になるのは使い心地です。
    単に「Sendという魔法の呪文を追加しないとコードがコンパイルされない」に近いのか、それとも「ワークスティーリング中に状態が壊れ、間欠的でデバッグしにくい形で失敗する」に近いのかが気になります。

    • だいたい次のようなコードを書くとします。
      Serverがあり、serveでメッセージを読み、その後各メッセージハンドラを新しいタスクとしてspawn(async move { ... })するような形です。
      最初はすべてうまく動きます。
      ところがある日、do_thisの実装を変えてthisの型がもはやSendではなくなると、spawn(...)が、匿名のasync move { }スコープによって作られた型がSendではないため不可だ、というおどろおどろしいコンパイルエラーを受け取ることになります。
      理由が必ずしも明確ではなく、エラーメッセージも役に立ちません。
      thisSendでなければ、do_that(arg).await.awaitをまたいで保持することはできません。
      .awaitはfutureが譲歩し、実行器によって別スレッドにスケジュールされ得る実行地点だからです。
      型をSendにできるなら問題ありません。
      しかしそうできない場合も十分にあり、その場合はfutureのスケジューリングをspawn_localのようなものに変えなければなりません。
      これを呼ぶには、かなりのボイラープレートを追加しなければならないこともあります。
      これがSendの問題です。
      単に型アノテーションを追加する問題ではなく、型がSendを実装しているかどうかは常に明確とは限らないため、コードに微妙に染み込み、後になって不明瞭な形で壊れることがあります。
    • 全般的に、コンパイラがSendSyncかどうかを検出します。
      プログラムをスレッドセーフに書けば問題ありません。
      要点はこれです。
      人々はTokio asyncがあちこちでSendSyncを要求するので難しいと不満を言いますが、実際にはどんな種類であれ安全な並行コードを書くこと自体が難しいのです。
      直感的ではなく、問題はasyncがそれを自動的に「処理してくれる」かのように感じさせる点にあります。
      しかし実際にはそうではありません。
      自分が何をしているのかを理解している必要があり、コンパイラはここで手助けしてくれるだけです。
      コアあたりスレッド方式にすると、状況によってはSendの必要性を隠せますが、すべてではありません。
      そして長期的には、アーキテクチャ面でしっぺ返しを受ける可能性があります。
    • コードは、必要な箇所であらゆるものがSend/Syncでなければコンパイルされません。
      間違っているかもしれませんが、これを達成するための怠惰な方法は、たいてい共有され得るものをArcMutexで包むことです。
    • コアあたりスレッドをデフォルトにすることに反対する論理は、単純化できると思います。
      CPU中心なら、ワークスティーリングのほうがほとんどの場合で優れているでしょう。
      I/O中心なら、コアあたりスレッドのほうがうまく機能するかもしれませんが、いずれにせよCPUには十分な余裕があるので、性能はそれほど重要ではありません。
      私の考えでは、ワークスティーリングのほうが言語APIに入れるデフォルトとして優れています。