代表的な面接トピック

Rust 2024 面接:async クロージャはどのような問題を解決するのか?

コーディング難しい
Offer.cc 編集チーム公開日 更新日

質問

Rust 2024 の async クロージャは `|| async {}` とどのように異なりますか?入力を借用し、キャンセルをサポートし、ライフタイムを保持する非同期コールバック API をどのように設計しますか?

プロンプトとスコープ

あなたはコールバックを非同期リトライ機構に渡す Rust 1.85 サービスを保守しています。コールバックは呼び出し元が所有するバッファを借用し、非同期 I/O を実行し、異なる借用ライフタイムで動作する必要があります。async || {}|| async {} を比較し、続いて AsyncFn、ライフタイム、キャプチャ、キャンセル、および移行について説明してください。

面接官がテストしていること

  • async クロージャの future がキャプチャされたデータを借用できることを理解しているか。
  • 高階非同期コールバックに対して AsyncFnAsyncFnMut、および AsyncFnOnce を使用しているか。
  • future が作成、ポーリング、またはキャンセルされたときに、所有権とライフタイムを区別できるか。
  • バージョン移行、境界テスト、およびリソースクリーンアップの計画を提示できるか。

確認すべき明確化のための質問

  1. コールバックは繰り返し呼び出されますか、キャプチャされた状態を変更しますか、それとも1回消費されますか?それによってトレイトが選択されます。
  2. 入力は所有されていますか、それとも短い借用ですか?呼び出し中に future が完了する必要がありますか?
  3. キャンセルは I/O 中に発生しますか、それともリトライ機構が終了するときですか?外部リソースはどのようにクリーンアップされますか?
  4. MSRV は Rust 1.85 に移行しましたか?また、依存関係は Rust 2024 をサポートしていますか?

30秒の回答フレームワーク

|| async {} は async ブロックを返す通常のクロージャであり、その内部 future は async クロージャと同じ借用関係を表現できません。Rust 1.85 では async || {} が第一級の async 呼び出しとなり、AsyncFn トレイトが追加されます。呼び出しパターンに応じてトレイトを選択し、入力の借用は future が完了するまで継続することを明記し、future の drop でキャンセルのクリーンアップを実行させます。まずツールチェーンとエディションを移行し、cargo fix を控えめに使用してから、コンパイラテストと実行時テストで借用、リトライ、キャンセルを検証します。

ステップバイステップの詳細解説

1. 2つの形式を比較する

通常のクロージャは future を返します:

rust
let old = |buf: &mut Vec<u8>| async move { buf.push(1); };

async クロージャは、非同期呼び出し自体をクロージャの契約の一部とします:

rust
let new = async |buf: &mut Vec<u8>| { buf.push(1); };

重要なポイントは、各呼び出しがその呼び出しの入力に関連付けられた future を生成できるかどうかです。最初の形式では高階ライフタイム制約を表現するのが困難になることがよくありますが、2番目の形式は AsyncFn トレイトによって表現されます。

2. AsyncFn トレイトの選択

繰り返し呼び出し可能な読み取り専用コールバックには AsyncFn を、繰り返しの呼び出しでキャプチャされた状態を変更する場合は AsyncFnMut を、コールバックがキャプチャを消費する場合は AsyncFnOnce を使用します。非同期であるという理由だけで、すべての future を BoxFuture に強制しないでください。有効な短い借用が拒否されてしまいます。

3. ライフタイムとキャプチャ

入力の借用は、future が完了するか drop されるまで存続する必要があります。コールバックはその借用を 'static タスクに配置してはならず、リトライ機構はバッファのライフタイムを超えて future をキューに入れてはなりません。真のバックグラウンド処理では、所有されたデータを最初にコピーまたはムーブして、タスクがそのライフタイムを所有できるようにします。

4. キャンセルとクリーンアップ

Rust には必須の非同期キャンセルプロトコルはありません。通常、future の drop がキャンセルを表します。I/O ラッパーは、drop 時または明示的なキャンセルトークンを介して、ソケットのクローズ、ロックの解放、一時ファイルの削除を行う必要があります。リトライ機構は、1つの future を同時にポーリングしたり、キャンセル後に状態を使用したりしてはなりません。

5. リトライと副作用

自動的にリトライすべきなのは冪等(idempotent)な操作のみです。非冪等な I/O には、リクエスト ID、トランザクション、または補償処理が必要です。試行ごとに新しい future を作成し、試行、エラー、およびキャンセル理由を記録します。外部の副作用がコミットされた可能性がある場合は、リトライする前に問い合わせを行うか冪等性キーを使用します。

6. 移行と検証

CI、開発環境、および MSRV を Rust 1.85 に移行し、Rust 2024 移行ガイドに従います。cargo fix --edition は保守的であり、意味論的なレビューの代わりにはなりません。短い借用、可変キャプチャ、繰り返し呼び出し、future の drop、タイムアウト、リトライ、およびサポートされているすべてのターゲットをテストします。

模範的な高クオリティの回答

コールバックが繰り返されるか、キャプチャを消費するか変更するかに応じて、AsyncFnAsyncFnMut、または AsyncFnOnce を選択します。async || は async クロージャを直接表現するため、各呼び出しの future をその入力借用に関連付けることができます。|| async {} を高階ジェネリクスでこのように制約するのはより困難です。短い借用の future を 'static として格納しません。バックグラウンドタスクは代わりに所有されたデータを受け取ります。future の drop またはキャンセルトークンが I/O とロックをクリーンアップします。リトライは冪等な操作に限定され、試行とリクエスト ID が記録されます。Rust 1.85 への移行後、コンパイラ、Miri、およびランタイムテストでライフタイム、リトライ、キャンセルをカバーします。

よくある間違い

  • 2つの形式が同一であると主張する → キャプチャされた借用および高階トレイトのセマンティクスを無視する → 短い借用のコールバックをテストする。
  • すべてのコールバックに 'static を要求する → 有効なフォアグラウンド借用を拒否する → 借用呼び出しと所有されたバックグラウンドデータを分離する。
  • ブール値のキャンセルフラグのみを使用する → I/O がロックやソケットを保持したままになる → drop およびトークンによるクリーンアップパスを提供する。
  • 非冪等な処理を無制限にリトライする → 副作用が重複する → リクエスト ID、クエリ、または補償処理を使用する。
  • cargo fix だけで移行が完了したとみなす → 意味論的リスクや MSRV リスクを残す → クロスターゲットテストおよび振る舞いテストを追加する。

フォローアップの質問と回答

なぜすべてのコールバック future を 'static として Box 化しないのですか?

入力借用に関連付けられたライフタイムが失われ、有効な短い借用のコールバックが拒否されてしまうためです。長時間のバックグラウンドタスクに入るデータのみ、最初に所有権を持たせてから 'static にする必要があります。

AsyncFnMut コールバックが並行して呼び出された場合はどうなりますか?

可変キャプチャを表現しますが、並行性の安全性は提供しません。呼び出しを直列化するか、同期を追加するか、各呼び出しに独立した状態を与えます。重複する可変借用を決して作成してはなりません。

I/O 中に future が drop されたときのクリーンアップをどのように証明しますか?

リソースを Drop または明示的なキャンセルパスでラップし、タイムアウトとタスクのキャンセルをテストし、接続、ロック、一時ファイル、および外部リクエスト ID の最終状態を検査します。

公開情報ソース

関連する質問

関連面接ツール

コーディング問題にはスクリーンショットを使用

問題をキャプチャし、制約条件、解法アプローチ、コード、エッジケース、計算量の順に進めます。

ツールを見る