はじめに
非同期処理のコードを書いていると、メソッドの引数にCancellationTokenが出てくることがよくあります。
public async Task<string> FetchDataAsync(CancellationToken cancellationToken)
なんとなく「キャンセルに関係するやつ」というのはわかるんですが、いざ自分で書こうとするとよくわからなくなってきます。
- そもそも何のためにあるの?
- どっちが渡す側で、どっちが受け取る側なの?
- ASP.NET Coreの機能じゃないの?
こういった疑問が浮かんだので、改めて調べてまとめました。
環境
- .NET 10.0
- C# 14.0
- Visual Studio 2026
そもそも何の問題を解決するのか
まず「CancellationTokenがない世界」を考えてみます。
たとえば、外部APIにリクエストを送る処理があるとします。
public async Task<string> FetchDataAsync()
{
var result = await httpClient.GetStringAsync("https://example.com/api/data");
return result;
}
この処理、ユーザーが「やっぱりいらない」と思ってキャンセルボタンを押したとしても、処理は走り続けます。止めようがありません。
これが困る場面は意外と多くて、
- ユーザーが画面を閉じたのに処理だけ走り続ける
- タイムアウトさせたいのに手段がない
- アプリをシャットダウンしようとしたら処理が終わるまで待たされる
といった場面があります。こういった状況を解決するのがCancellationTokenです。
基本的な使い方
コードで順を追って見てみます。
キャンセルを発行する側
// CancellationTokenSourceを作る
var cts = new CancellationTokenSource();
// TokenをSourceから取り出して処理に渡す
var task = FetchDataAsync(cts.Token);
// 任意のタイミングでキャンセルを発火
cts.Cancel();
キャンセルを受け取る側
public async Task<string> FetchDataAsync(CancellationToken cancellationToken)
{
// 受け取ったTokenをそのまま渡すだけでOK
var result = await httpClient.GetStringAsync("https://example.com/api/data", cancellationToken);
return result;
}
受け取る側は、受け取ったTokenを自分の内部で使っているメソッドにそのまま渡していくだけです。自分で「キャンセルされたかどうか」をチェックするコードを書かなくていいのは楽だと思いました。
呼び出し元でキャッチする
キャンセルが発火するとOperationCanceledExceptionがスローされるので、呼び出し元でキャッチします。
try
{
var result = await FetchDataAsync(cts.Token);
Console.WriteLine(result);
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました");
}
finally
{
cts.Dispose(); // 使い終わったらDisposeする
}
OperationCanceledExceptionは「異常」ではなく「正常なキャンセル」なので、エラーログに記録するのではなく、静かに処理を終わらせるケースが多いと思います。
タイムアウトにも使える
「一定時間でキャンセルしたい」という場面にもそのまま使えます。
// 5秒でタイムアウト
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var result = await FetchDataAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("タイムアウトしました");
}
コンストラクタにTimeSpanを渡すだけで、指定した時間が経過すると自動でキャンセルされます。
外部API呼び出しにタイムアウトを設けたい場面などでよく使うパターンです。
「なぜ外から渡すのか」がわかると設計の意図が見えてくる
最初、「なぜメソッドの中でCancellationTokenSourceを作らないのか」と思いました。
// これではダメなの?
public async Task<string> FetchDataAsync()
{
var cts = new CancellationTokenSource();
var result = await httpClient.GetStringAsync("https://example.com/api/data", cts.Token);
return result;
}
これだと、メソッドの外から「キャンセルして」と伝える手段がなくなってしまいます。キャンセルのタイミングを決めるのは呼び出し元の責任なので、Tokenは外から渡す設計になっているわけです。
ユーザーのボタン押下、タイムアウト、アプリのシャットダウンなど、「何をトリガーにキャンセルするか」はメソッドの中身とは別の話です。この関心を分離するために、Tokenを引数で受け渡す設計になっているんだと理解しました。
CancellationTokenはC#の機能、ASP.NET Coreの機能ではない
ASP.NET Coreで実装を進めていると、コントローラーやMinimal APIのメソッド引数にCancellationTokenが自然と出てくることがあります。そこで「これってASP.NET Coreの機能なの?」と思いました。
結論を言うと、CancellationToken自体はC#(.NET)の機能です。System.Threading名前空間に定義されており、ASP.NET Coreとは関係なくコンソールアプリでもバックグラウンドサービスでも普通に使えます。
ASP.NET Coreは、コントローラーやMinimal APIでCancellationTokenを自動で注入してくれる仕組みを持っているだけです。
// ASP.NET Coreでは引数に書くだけで自動注入される
app.MapGet("/data", async (CancellationToken cancellationToken) =>
{
var result = await FetchDataAsync(cancellationToken);
return result;
});
ユーザーが通信を切断したりページを閉じたりすると、フレームワークが自動でキャンセルを発火してくれます。これはASP.NET Core固有の便利な機能ですが、CancellationToken自体はその下にある汎用的な仕組みです。
まとめ
- CancellationTokenは「外から非同期処理を止めるための仕組み」で、C#(.NET)の機能
- CancellationTokenSourceがキャンセルを発行する側、CancellationTokenを引数で受け取って処理に流していく
- キャンセル時は
OperationCanceledExceptionがスローされる - コンストラクタにTimeSpanを渡せばタイムアウトとしても使える
- 外から渡す設計になっているのは、「いつキャンセルするか」の決定権を呼び出し元に持たせるため
- ASP.NET Coreはこの仕組みに乗っかって、自動注入という形で便利に使えるようにしているだけ
なんとなく使っていたCancellationTokenですが、「なぜこういう設計になっているか」まで理解すると、引数の設計や渡し方に納得感が出てきました。
参考になったら いいね や ストック をお願いします!
同じような経験をされた方のコメントもお待ちしています。
参考
- CancellationToken 構造体 - .NET(Microsoft Learn)
- CancellationTokenSource クラス - .NET(Microsoft Learn)
- マネージド スレッドのキャンセル - .NET(Microsoft Learn)
関連リンク
技術ブログでも学びや検証内容をまとめています。
【PR】株式会社 ONE WEDGE
株式会社ONE WEDGE は、Webシステム開発・SES・AI/DX支援を行うIT企業です。
生成AIを活用した業務効率化や次世代システム開発にも注力しており、企業の課題解決だけでなく、エンジニア一人ひとりの成長にも本気で向き合っています。
また、技術は「一人で学ぶもの」ではなく、「仲間と成長するもの」だと考え、社内外でのコミュニティづくりにも力を入れています。
「ITエンジニアに、IT業界に貢献する企業」をテーマに、AI時代に挑戦し続けられる組織を目指しています。