CancellationToken の .NET プログラマー向けガイド

公開: 2022-08-23

たまにはキャンセルもいいものです。 私の .NET プロジェクトの多くでは、内部プロセスと外部プロセスの両方をキャンセルする十分な動機がありました。 Microsoft は、開発者がさまざまな複雑な実装でこの一般的なユース ケースに取り組んでいることを知り、より良い方法が必要であると判断しました。 したがって、一般的なキャンセル通信パターンがCancellationTokenとして導入されました。これは、低レベルのマルチスレッドとプロセス間通信構造を使用して構築されました。 このパターンに関する私の最初の調査の一環として、そして Microsoft の実装のために実際の .NET ソース コードを掘り下げた後、 CancellationTokenがより広範な一連の問題を解決できることを発見しました。トリガー、およびフラグを介した一般的なプロセス間通信。

意図された CancellationToken ユースケース

CancellationTokenは、操作をキャンセルするための既存のソリューションを強化および標準化する手段として、.NET 4 で導入されました。 一般的なプログラミング言語が実装する傾向があるキャンセルを処理するための 4 つの一般的なアプローチがあります。

殺す教えて、ノーと答えないで丁寧に質問し、拒否を受け入れるフラグを丁寧に設定し、必要に応じてポーリングさせます
アプローチハードストップ; 不一致は後で解決するやめるように言いますが、物事を片付けさせますやめるという直接的だが穏やかな要求やめるように頼みますが、強制しないでください
概要腐敗と苦痛への確実な道きれいな停止ポイントを許可しますが、停止する必要がありますクリーンな停止ポイントを許可しますが、キャンセル リクエストは無視される場合がありますキャンセルはフラグを介してリクエストされます
Pスレッドpthread_kill
pthread_cancel (非同期)
pthread_cancel (遅延モード) なし旗を通して
。ネットThread.Abort なしThread.Interrupt CancellationTokenのフラグを介して
ジャワThread.destroy
Thread.stop
なしThread.interrupt フラグまたはThread.interruptedを介して
パイソンPyThreadState_SetAsyncExc なしasyncio.Task.cancel 旗を通して
ガイダンス受け入れられません。 このアプローチを避ける特に言語が例外や巻き戻しをサポートしていない場合は許容されます言語がサポートしている場合は許容されますより良いが、より多くのグループの努力
キャンセル方法の概要と言語の例

CancellationTokenは、キャンセルの会話が協調的である最後のカテゴリにあります。

Microsoft がCancellationTokenを導入した後、特に多くの主要な .NET API がこれらのトークンをネイティブに使用するように更新されたため、開発コミュニティはすぐにそれを受け入れました。 たとえば、ASP.NET Core 2.0 以降、アクションはオプションのCancellationTokenパラメーターをサポートします。このパラメーターは、HTTP 要求が閉じられた場合に通知することができ、操作をキャンセルできるため、リソースの不必要な使用を回避できます。

.NET コードベースを深く掘り下げると、 CancellationTokenの使用がキャンセルに限定されないことが明らかになりました。

顕微鏡下の CancellationToken

CancellationTokenの実装を詳しく見ると、単純なフラグ (つまり、 ManualResetEvent ) と、そのフラグを監視および変更する機能を提供するサポート インフラストラクチャであることがわかります。 CancellationTokenの主なユーティリティはその名前にあり、これが操作をキャンセルする一般的な方法であることを示唆しています。 最近では、非同期または長時間実行される操作を含む .NET ライブラリ、パッケージ、またはフレームワークは、これらのトークンを使用してキャンセルできます。

CancellationTokenは、そのフラグを手動で「true」に設定するか、特定の期間が経過した後に「true」に変更するようにプログラムすることによってトリガーできます。 CancellationTokenがトリガーされる方法に関係なく、このトークンを監視しているクライアント コードは、次の 3 つの方法のいずれかを使用してトークン フラグの値を決定できます。

  • WaitHandleの使用
  • CancellationTokenのフラグをポーリングする
  • プログラムによるサブスクリプションを通じて、フラグの状態が更新されたときにクライアント コードに通知する

.NET コードベースをさらに調査した結果、.NET チームはCancellationTokensがキャンセルに関連しない他のシナリオで役立つことを発見したことが明らかになりました。 複雑な状況を簡素化するマルチスレッドおよびプロセス間調整を C# 開発者に提供する、これらの高度でブランド外のユース ケースのいくつかを見てみましょう。

高度なイベントの CancellationTokens

ASP.NET Core アプリケーションを作成するとき、アプリケーションがいつ開始されたかを知る必要がある場合や、ホストのシャットダウン プロセスにコードを挿入する必要がある場合があります。 そのような場合、 IHostApplicationLifetimeインターフェイス (以前はIApplicationLifetime ) を使用します。 このインターフェイス (.NET Core のリポジトリから) は、 CancellationTokenを使用して、 ApplicationStartedApplicationStopping 、およびApplicationStoppedの 3 つの主要なイベントを通信します。

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }

一見すると、 CancellationTokenは特にイベントとして使用されているため、ここに属していないように見えるかもしれません。 ただし、さらに詳しく調べると、これらのトークンが完全に適合することがわかります。

  • それらは柔軟性があり、インターフェースのクライアントがこれらのイベントをリッスンする複数の方法を可能にします。
  • それらはすぐにスレッドセーフです。
  • CancellationTokenを組み合わせることで、さまざまなソースから作成できます。

CancellationTokenはすべてのイベントのニーズに完全に対応できるわけではありませんが、アプリケーションの開始や停止など、一度だけ発生するイベントには理想的です。

タイムアウトの CancellationToken

デフォルトでは、ASP.NET をシャットダウンする時間はほとんどありません。 もう少し時間が必要な場合は、組み込みのHostOptionsクラスを使用して、このタイムアウト値を変更できます。 その下で、このタイムアウト値はCancellationTokenにラップされ、基礎となるサブプロセスに供給されます。

IHostedServiceStopAsyncメソッドは、この使用法の良い例です。

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }

IHostedServiceインターフェイスの定義から明らかなように、 StopAsyncメソッドは 1 つのCancellationTokenパラメーターを受け取ります。 そのパラメーターに関連付けられたコメントは、 CancellationTokenに対する Microsoft の当初の意図がキャンセル プロセスではなくタイムアウト メカニズムであったことを明確に伝えています。

私の意見では、このインターフェイスがCancellationTokenの存在よりも前に存在していた場合、これはTimeSpanパラメーターであり、停止操作の処理が許可されている期間を示していた可能性があります。 私の経験では、タイムアウトのシナリオは、ほとんどの場合、優れた追加ユーティリティを使用してCancellationTokenに変換できます。

ここでは、 StopAsyncメソッドがどのように設計されているかを知っていることを忘れて、このメソッドのコントラクトをどのように設計するかを考えてみましょう。 まず、要件を定義しましょう。

  • StopAsyncメソッドは、サービスの停止を試行する必要があります。
  • StopAsyncメソッドには、適切な停止状態が必要です。
  • 正常な停止状態が達成されたかどうかに関係なく、タイムアウト パラメータで定義されているように、ホステッド サービスには停止する最大時間が必要です。

任意の形式でStopAsyncメソッドを使用することにより、最初の要件を満たします。 残りの要件は注意が必要です。 CancellationTokenは、標準の .NET フラグベースの通信ツールを使用して会話を強化することで、これらの要件を正確に満たします。

通知メカニズムとしての CancellationToken

CancellationTokenの背後にある最大の秘密は、それが単なるフラグであることです。 CancellationTokenを使用して、プロセスを停止する代わりにプロセスを開始する方法を説明しましょう。

次の点を考慮してください。

  1. RandomWorkerクラスを作成します。
  2. RandomWorkerには、ランダムな作業を実行するDoWorkAsyncメソッドが必要です。
  3. DoWorkAsyncメソッドでは、呼び出し元がいつ作業を開始するかを指定できる必要があります。
 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

上記のクラスは最初の 2 つの要件を満たし、3 番目の要件が残ります。 タイム スパンや単純なフラグなど、ワーカーをトリガーするために使用できる代替インターフェイスがいくつかあります。

 # With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();

これら 2 つのアプローチは問題ありませんが、 CancellationTokenを使用するほど洗練されたものはありません。

 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

このサンプル クライアント コードは、この設計の威力を示しています。

 using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }

CancellationTokenSourceは、舞台裏でCancellationTokenを作成し、関連するすべてのプロセスのトリガーを調整します。 この場合、関連するプロセスはRandomWorkerで、開始を待っています。 このアプローチにより、デフォルトのCancellationToken実装に組み込まれているスレッド セーフを活用できます。

広範な CancellationToken ツールボックス

これらの例は、 CancellationTokenが意図したユース ケース以外で役立つソリューションのツールボックスをどのように提供するかを示しています。 ツールは、プロセス間フラグベースの通信を含む多くのシナリオで役立ちます。 タイムアウト、通知、または 1 回限りのイベントのいずれに直面しても、Microsoft によってテストされたこの洗練された実装に頼ることができます。

上から順に、"Gold" (金色)、"Microsoft"、"Partner" (どちらも黒) の文字が表示され、その後に Microsoft のロゴが表示されます。
Microsoft ゴールド パートナーとして、Toptal は Microsoft エキスパートのエリート ネットワークです。 いつでもどこでも、必要なときに必要な専門家と一緒に高性能チームを構築しましょう!

Toptal Engineering ブログの詳細情報:

  • .NET Core: ワイルドでオープン ソースへ。 マイクロソフト、どうしてそんなに時間がかかったの?!
  • ASP.NET Core を使用して ASP.NET Web API を構築する
  • .NET プロジェクトをブートストラップして作成する方法