代表的な面接トピック

C++26 コーディング面接:キャンセル安全なマルチスケジューラパイプラインをどのように実装するか?

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

質問

C++26 の std::execution を使用して、読み取り、並列変換、集約のパイプラインを実装してください。スケジューラの切り替え、並行性の制限、停止の伝播、および完了までオペレーション状態とリソースを生存させ続ける処理をどのように行いますか?

プロンプトと適用可能性

あるサービスで、読み取り、変換、集約を非同期パイプラインとして構成する必要があります。呼び出し元はタイムアウト、切断、またはリソース圧迫によりキャンセルする可能性があり、他の処理がすでに実行中であっても各ステージが失敗する可能性があります。C++26 の std::execution sender/receiver モデルを使用して、スケジューリング、完了シグナル、ライフタイム、およびフォールバックパスを説明してください。

これは特定のライブラリの構文の暗記ではなく、並行性抽象化の境界をテストするものです。標準の execution-control ライブラリは、sender の作業グラフを receiver の完了処理から分離し、接続された非同期状態のためにオペレーション状態(operation state)を使用します。優れた回答では、キャンセル、エラーセマンティクス、クリーンアップが明示的な規約として定義されます。

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

  • 遅延(lazy)sender、接続されたオペレーション状態、および start における実際の実行の区別。
  • ビジネスコード内で場当たり的なスレッドを作成するのではなく、スケジューラによって実行リソースを表現すること。
  • キャンセルを例外として偽装するのではなく、set_valueset_errorset_stopped を個別に処理すること。
  • すべてのステージを通じて停止要求を伝播し、停止後に新たな副作用が発生するのを防止すること。
  • 並列処理におけるバックプレッシャー、制限、例外安全性、および集約について説明すること。
  • 標準ライブラリが利用できない場合に、機能検出、互換性レイヤー、および一貫したテストを提供すること。

最初に確認すべき明確化事項

  1. 読み取りはローカルファイル、ネットワークリクエスト、またはデータベースカーソルのどれから行われますか?各ステージは再試行可能ですか、それとも外部への副作用がありますか?
  2. キャンセルとは、即時停止、中断不可能な呼び出しの完了後の停止、またはコミット済み結果のロールバックのどれを意味しますか?
  3. 並列度、メモリ、アイテムごとのタイムアウト、および全体のデッドラインの制限はどのようなものですか?
  4. 集約では、入力順序、安定した浮動小数点結果、または可視の途中結果を維持する必要がありますか?
  5. 対象のコンパイラとライブラリは C++26 execution を実装していますか、それとも実験的な実装のみですか?

30秒での回答

value、error、stopped の各チャネルを持つ完了規約を定義します。キャンセルは開始されていない作業を防止し、中断可能なステージが迅速に応答できるようにします。各 sender は遅延状態を維持します。接続によってオペレーション状態が作成され、start で実行が開始されます。明示的なスケジューラが実行リソースを所有し、並列度とキューの制限によってメモリを保護します。アグリゲータは順序付けと途中結果のルールを定義します。機能検出により、標準実装、互換性ライブラリ、または同期スカラーパスが選択され、キャンセル、エラー、および結果に関する共通のテストが適用されます。

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

1. 遅延作業グラフを描く

読み取り sender を変換 sender に接続し、次に集約 sender に接続します。then は生成された値を次のノードに渡し、let_value は結果から別の非同期オペレーションを作成でき、when_all は並列ブランチを表します。合成はグラフを構築するものであり、構築中に I/O を実行すべきではありません。

2. 接続とライフタイムを定義する

sender と receiver 間の connect はオペレーション状態を作成し、実行は start の後にのみ許可されます。オペレーション状態のアドレスは非同期オペレーションが完了するまで有効であり続ける必要があるため、戻り処理が近いスタックフレーム内に配置することはできません。リクエストコンテキストまたは非同期スコープがこれを所有し、value、error、stopped の各パスでリソースを解放する必要があります。

3. スケジューラにリソースを割り当てる

スケジューラは、実行リソースへの軽量なハンドルです。読み取りを I/O リソースに、CPU 変換を制限付き並列リソースに配置します。ステージの境界を表現するには onstarts_on、または continues_on を使用します。要素ごとにスレッドを作成してはいけません。メモリとコンテキストスイッチを制御するために、並列度、キュー長、およびバッチサイズを制限します。

4. stopped、error、value を伝播する

値の完了(value completion)は次のステージに進み、エラーは統合エラー処理に入り、停止(stopped)はキャンセル処理に入ります。receiver 環境のストップトークン(stop token)は、キャンセルの監視ポイントとなります。ブロッキングシステムコールには、中断可能なインターフェースまたは制限付きタイムアウトが必要です。そうでなければ、リターンした後にしか応答できません。キャンセルはロールバックではありません。外部への書き込みが一度発生した場合は、冪等キー、補償処理、または明示的な不可逆境界を使用します。

5. 最小限の合成スケッチ

以下のコードはグラフの構造を示しています。実際の読み取りおよびスレッドプール sender はプロジェクトから提供されます。

cpp
using namespace std::execution;

auto pipeline = read_sender()
  | let_value([](Batch batch) {
      return bulk_transform(batch, get_parallel_scheduler());
    })
  | then([](Transformed value) { return summarize(value); })
  | upon_error([](std::exception_ptr error) { record_failure(error); })
  | upon_stopped([] { record_cancellation(); });

auto state = connect(std::move(pipeline), receiver);
start(state);

receiver は、その環境でストップトークンを提供する有効なリクエストスコープによって所有される必要があります。本番コードでは、単に一般的な失敗を公開するのではなく、ステージ、バッチ、デッドライン、およびキャンセル理由も記録する必要があります。

6. 並列集約と副作用の境界

並列変換中はタスクごとのローカル状態を保持し、集約時に定義された順序でマージします。順不同のマージが許可されている場合は、非結合的な浮動小数点演算によって生じる差分を明記します。安定した出力が必要な場合は、インデックスまたはパーティションシーケンスを保持します。外部書き込みの前にストップトークンを確認し、コミット後に冪等キーを記録します。set_stopped は、コミットが取り消されたことを意味するわけではありません。

7. フォールバック、テスト、および可観測性

機能テストマクロ、コンパイラバージョン、およびライブラリ機能からマトリックスを構築します。標準の execution が利用できない場合、互換性実装によって内部の sender 規約を維持できます。それ以外の場合は、value、error、stopped のセマンティクスを一貫させながら、制限付きスレッドプールまたは同期パスを使用します。空の入力、部分バッチ、繰り返しのキャンセル、エラー対停止の競合、リソース枯渇、オペレーション状態の早期破棄、および繰り返しの開始をテストします。スループット、テールレイテンシ、キュー長、キャンセル応答時間、および未完了タスクをベンチマークします。

質の高い模範解答

パイプラインを遅延 sender グラフとしてモデル化します。読み取り、並列変換、集約がそれぞれ完了シグネチャを公開し、connect がオペレーション状態を作成し、start がそれを実行します。I/O と CPU は異なるスケジューラを使用し、並列度、キュー、バッチの制限を設けます。receiver は value、error、stopped を個別に処理し、中断可能なすべてのポイントでストップトークンをチェックします。外部への書き込みには冪等性と補償の境界を使用するため、キャンセルがロールバックを保証することはありません。

リクエストスコープは完了までオペレーション状態を所有し、エラーパスと停止パスの両方でクリーンアップが共有されます。ツールチェーンは C++26 execution を検出し、標準、互換性実装、または同期フォールバックを選択します。すべてのパスは、空のバッチ、競合、キャンセル応答、リソース枯渇、および早期破棄に関する振る舞いテストを共有します。本番環境では、テールレイテンシ、キューの深さ、キャンセル応答、およびリークを監視して、並列化によって目標とするメトリクスが改善されていることを検証します。

よくある間違い

  • sender の構築を非同期作業の開始とみなすこと。
  • 関数がリターンしたときにオペレーション状態が破棄されるようにすること。
  • 例外チャネルのみを使用し、キャンセルを通常のエラーとして扱うこと。
  • キュー、並列度、またはメモリの制限なしに要素ごとにスレッドを作成すること。
  • 停止シグナルが届いたときに外部の副作用がロールバックされたと主張すること。
  • 並列集約において、順序付け、浮動小数点許容量、または途中結果のルールを未定義のままにすること。
  • 機能検出やフォールバックなしに標準ライブラリのパスを 1 つだけ実装すること。

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

sender は実際にいつ実行されますか?

合成はグラフを記述します。接続によってオペレーション状態が作成され、start によって非同期オペレーションが開始されます。テストでは、構築、接続、開始を個別にカバーする必要があります。

停止要求によってシステムコールを強制終了できますか?

一般的にはできません。呼び出しには中断可能なインターフェース、タイムアウト、またはチャンクごとのチェックが必要です。そうでない場合、リターンした後に応答するため、最悪の応答時間を測定する必要があります。

エラーと停止が同時に発生した場合はどうなりますか?

receiver が正確に 1 つの終端シグナルを受け取るように、優先順位と単一完了ルールを定義します。診断のために、元のエラーと停止理由の両方を保持します。

when_all の 1 つのブランチが失敗した場合はどうなりますか?

他のブランチが継続するか、停止要求を受け取るか、またはクリーンアップを完了するかを指定します。共有リソースにはスコープ化された所有権とキャンセルの伝播が必要です。アグリゲータは無効なブランチ結果を読み取ってはいけません。

集約を再現可能にするにはどうすればよいですか?

パーティションのシーケンス番号を保持して固定順序でマージするか、誤差範囲を指定して順不同の結果を明示的に許可します。並列リダクションでは浮動小数点の結合法則を前提にできません。

本番ライブラリに C++26 execution がない場合はどうしますか?

コンパイラ機能マトリックスを使用して、完了セマンティクスとテストを維持しながら互換性実装または同期パスを選択します。実験的ライブラリのプライベート型をパブリックインターフェースに公開してはいけません。

公開情報ソース

関連する質問

関連面接ツール

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

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

ツールを見る