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
を使用して、 ApplicationStarted
、 ApplicationStopping
、および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
にラップされ、基礎となるサブプロセスに供給されます。
IHostedService
のStopAsync
メソッドは、この使用法の良い例です。
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
を使用して、プロセスを停止する代わりにプロセスを開始する方法を説明しましょう。
次の点を考慮してください。
-
RandomWorker
クラスを作成します。 -
RandomWorker
には、ランダムな作業を実行するDoWorkAsync
メソッドが必要です。 -
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 によってテストされたこの洗練された実装に頼ることができます。
Toptal Engineering ブログの詳細情報:
- .NET Core: ワイルドでオープン ソースへ。 マイクロソフト、どうしてそんなに時間がかかったの?!
- ASP.NET Core を使用して ASP.NET Web API を構築する
- .NET プロジェクトをブートストラップして作成する方法