C# CancellationTokenSourceの使い方を完全解説|非同期処理のキャンセル・タイムアウト実装例

はじめに

C#で非同期処理を実装していると、「処理を途中で止めたい」「一定時間を過ぎたらタイムアウトさせたい」「画面を閉じたらバックグラウンド処理も止めたい」といった場面がよくあります。

そのようなキャンセル制御で中心になるのが CancellationTokenSource です。

CancellationTokenSource を正しく使うと、async/awaitTask.RunHttpClient、ファイル処理、ASP.NET Coreのリクエスト処理などで、安全にキャンセル可能な処理を実装できます。

一方で、Cancel() を呼んだだけで処理が即座に止まるわけではありません。C#のキャンセルは「強制停止」ではなく「協調的キャンセル」です。処理側が CancellationToken を受け取り、キャンセル要求を定期的に確認して、適切なタイミングで終了する必要があります。

この記事では、C#の CancellationTokenSource の基本から、タイムアウト、複数キャンセル条件、例外処理、実践的な実装例、ベストプラクティスまでを体系的に解説します。

1. C#のCancellationTokenSourceとは?非同期処理を安全にキャンセルする仕組み

1-1. CancellationTokenSourceの役割

CancellationTokenSource は、キャンセル要求を発行するためのオブジェクトです。

C#では、非同期処理や長時間実行される処理を外部から安全に止めるために、CancellationTokenSourceCancellationToken を組み合わせて使います。

基本的な役割は次のとおりです。

C#
using var cts = new CancellationTokenSource();

CancellationToken token = cts.Token;

// キャンセルを要求する
cts.Cancel();

CancellationTokenSourceToken プロパティを通じて CancellationToken を提供し、Cancel()CancelAfter() によってキャンセル要求を通知する役割を持ちます。Microsoftの公式ドキュメントでも、CancellationTokenSourceToken プロパティでトークンを提供し、Cancel または CancelAfter によってキャンセルメッセージを送るオブジェクトとして説明されています。Microsoft Learn+1

重要なのは、CancellationTokenSource が処理を直接停止するわけではない点です。あくまで「キャンセルしてほしい」という通知を出すだけです。

1-2. CancellationTokenとの違い

CancellationTokenSourceCancellationToken は似ていますが、役割が明確に違います。

CancellationTokenSource はキャンセル要求を出す側です。一方、CancellationToken はキャンセル要求を受け取る側に渡すための値です。

C#
using var cts = new CancellationTokenSource();

// 処理側には Token だけを渡す
await DoWorkAsync(cts.Token);

// キャンセルを発行できるのは CancellationTokenSource
cts.Cancel();

メソッドに渡すべきなのは、基本的に CancellationTokenSource ではなく CancellationToken です。

C#
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}

この設計により、処理を実行する側はキャンセル要求を「検知」できますが、勝手にキャンセルを「発行」することはできません。キャンセルの主導権を呼び出し元に残せるため、責務が分離されます。

1-3. なぜ非同期処理にキャンセル制御が必要なのか

非同期処理では、処理が完了するまでに時間がかかることがあります。

たとえば、次のような処理です。

C#
await httpClient.GetAsync("https://example.com/api/data");
await File.ReadAllTextAsync("large-file.txt");
await Task.Run(() => HeavyCalculation());

これらの処理をキャンセルできないと、ユーザーが画面を閉じても処理が続いたり、通信待ちでアプリケーションの応答性が低下したり、不要なCPU・メモリ・ネットワーク資源を消費したりします。

キャンセル制御を入れることで、次のようなメリットがあります。

  • ユーザー操作に応じて処理を中断できる

  • タイムアウトを実装できる

  • 不要になったバックグラウンド処理を止められる

  • Webリクエスト中断時にサーバー側処理も止められる

  • アプリケーション全体の安定性を高められる

特に非同期処理では、キャンセルを「例外的な異常」ではなく「通常あり得る制御フロー」として設計することが重要です。

1-4. Task・async/awaitとの関係

CancellationTokenSource は、Taskasync/await と一緒に使われることが多いです。

たとえば Task.Delay には CancellationToken を渡せます。

C#
using var cts = new CancellationTokenSource();

Task task = Task.Delay(10000, cts.Token);

cts.Cancel();

try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}

await している処理がキャンセルに対応している場合、キャンセル要求を受けると OperationCanceledException が発生します。

この例外は「エラー」というより、キャンセルされたことを呼び出し元に伝えるための仕組みです。キャンセル可能な非同期APIでは、CancellationToken がキャンセルされると、返された TaskOperationCanceledException が格納される場合があります。たとえば HttpClientHandler.SendAsync の公式ドキュメントでも、キャンセルトークンがキャンセルされた場合は OperationCanceledException が返却タスクに格納されると説明されています。Microsoft Learn+1

2. CancellationTokenSourceの基本的な使い方

2-1. CancellationTokenSourceを生成する

もっとも基本的な生成方法は、new CancellationTokenSource() です。

C#
var cts = new CancellationTokenSource();

ただし、CancellationTokenSource は使い終わったら Dispose() するのが基本です。using または using var を使うと安全です。

C#
using var cts = new CancellationTokenSource();

CancellationTokenSource.Dispose() の公式ドキュメントでは、使用が終わったら Dispose を呼ぶこと、また Dispose 後の CancellationTokenSource は使用できない状態になることが説明されています。Microsoft Learn

長時間保持する必要がない場合は、次のように using var を使うのがおすすめです。

C#
public async Task ExecuteAsync()
{
using var cts = new CancellationTokenSource();

await DoWorkAsync(cts.Token);
}

2-2. TokenプロパティをTaskに渡す

CancellationTokenSource から Token プロパティを取得し、キャンセル可能にしたい処理へ渡します。

C#
using var cts = new CancellationTokenSource();

CancellationToken token = cts.Token;

await Task.Delay(5000, token);

独自メソッドにも CancellationToken を引数として渡します。

C#
public async Task DownloadAsync(CancellationToken cancellationToken)
{
await Task.Delay(3000, cancellationToken);
}

呼び出し側は次のようになります。

C#
using var cts = new CancellationTokenSource();

await DownloadAsync(cts.Token);

この形にしておくと、呼び出し元がキャンセルのタイミングを制御できます。

2-3. Cancelメソッドでキャンセルを要求する

キャンセルしたいタイミングで Cancel() を呼びます。

C#
using var cts = new CancellationTokenSource();

Task task = DoWorkAsync(cts.Token);

// 何らかの条件でキャンセル
cts.Cancel();

try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました。");
}

Cancel() は「処理を止める命令」ではなく「キャンセル要求を通知するメソッド」です。

そのため、処理側が CancellationToken を確認していない場合、Cancel() を呼んでも処理は止まりません。

2-4. IsCancellationRequestedでキャンセル状態を確認する

CancellationToken.IsCancellationRequested を使うと、キャンセルが要求されているかどうかを確認できます。

C#
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("キャンセル要求を検知しました。");
return;
}

Console.WriteLine($"処理中: {i}");
await Task.Delay(1000);
}
}

IsCancellationRequested を使うと、例外を投げずに処理を自然に終了できます。

ただし、呼び出し元に「キャンセルされた」という状態を明確に伝えたい場合は、次に紹介する ThrowIfCancellationRequested() を使う方が適しています。

2-5. ThrowIfCancellationRequestedで処理を中断する

ThrowIfCancellationRequested() は、キャンセル要求がある場合に OperationCanceledException を投げます。

C#
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine($"処理中: {i}");
await Task.Delay(1000, cancellationToken);
}
}

この書き方のメリットは、呼び出し元が try-catch でキャンセルを一元的に扱えることです。

C#
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}

非同期メソッドでは、ThrowIfCancellationRequested() とキャンセル対応APIへの CancellationToken 渡しを組み合わせるのが基本です。

3. async/awaitでCancellationTokenSourceを使う実装例

3-1. 非同期メソッドにCancellationTokenを渡す基本形

非同期メソッドをキャンセル可能にする場合、メソッドの最後の引数として CancellationToken を受け取る形が一般的です。

C#
public async Task<string> GetMessageAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
return "完了しました";
}

呼び出し側では CancellationTokenSource を作成し、Token を渡します。

C#
using var cts = new CancellationTokenSource();

try
{
string result = await GetMessageAsync(cts.Token);
Console.WriteLine(result);
}
catch (OperationCanceledException)
{
Console.WriteLine("処理がキャンセルされました。");
}

キャンセル不要な呼び出しも考慮するなら、既定値を指定できます。

C#
public async Task<string> GetMessageAsync(
CancellationToken cancellationToken = default)
{
await Task.Delay(1000, cancellationToken);
return "完了しました";
}

3-2. Task.Run内でキャンセルを検知する例

CPU負荷の高い処理を Task.Run でバックグラウンド実行する場合も、CancellationToken を使えます。

C#
public async Task ExecuteHeavyWorkAsync(CancellationToken cancellationToken)
{
await Task.Run(() =>
{
for (int i = 0; i < 100000; i++)
{
cancellationToken.ThrowIfCancellationRequested();

// 重い処理の例
DoHeavyCalculation(i);
}
}, cancellationToken);
}

private void DoHeavyCalculation(int value)
{
// CPU負荷の高い処理を想定
}

Task.RunCancellationToken を渡すだけでなく、実際の処理内部でも ThrowIfCancellationRequested() を呼ぶことが重要です。

C#
cancellationToken.ThrowIfCancellationRequested();

Task.Run にトークンを渡しても、実行中の処理が自動的に中断されるわけではありません。ループや長時間処理の中で、キャンセル状態を明示的に確認する必要があります。

3-3. ループ処理を途中でキャンセルする例

キャンセル制御が特に重要なのは、ループ処理です。

C#
public async Task ProcessItemsAsync(
IEnumerable<string> items,
CancellationToken cancellationToken)
{
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine($"処理中: {item}");

await Task.Delay(500, cancellationToken);
}
}

呼び出し側は次のようになります。

C#
using var cts = new CancellationTokenSource();

var task = ProcessItemsAsync(
new[] { "A", "B", "C", "D", "E" },
cts.Token);

// 2秒後にキャンセル
cts.CancelAfter(TimeSpan.FromSeconds(2));

try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("ループ処理をキャンセルしました。");
}

ループの各ステップで ThrowIfCancellationRequested() を呼ぶことで、キャンセル要求に素早く反応できます。

3-4. OperationCanceledExceptionを適切に扱う方法

キャンセル時には、一般的に OperationCanceledException を捕捉します。

C#
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
Console.WriteLine("ユーザー操作またはタイムアウトによりキャンセルされました。");
}

when 条件を使うと、自分が管理している CancellationTokenSource によるキャンセルかどうかを確認できます。

注意点として、Exception でまとめて捕捉してログに「エラー」として出してしまうと、正常なキャンセルまで障害扱いになってしまいます。

C#
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
// キャンセルは想定内の制御フローとして扱う
}
catch (Exception ex)
{
// 本当のエラーだけを処理する
Console.WriteLine(ex);
}

このように、キャンセルと通常の例外は分けて扱うのが基本です。

3-5. キャンセル時に後処理を実行する方法

キャンセルされた場合でも、ファイル、ストリーム、ロック、DB接続などの後処理は確実に実行する必要があります。

そのためには try-finally を使います。

C#
public async Task DoWorkWithCleanupAsync(CancellationToken cancellationToken)
{
Console.WriteLine("処理開始");

try
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine($"処理中: {i}");
await Task.Delay(1000, cancellationToken);
}
}
finally
{
Console.WriteLine("後処理を実行します。");
}
}

呼び出し側でキャンセルされた場合でも、finally ブロックは実行されます。

C#
using var cts = new CancellationTokenSource();

cts.CancelAfter(TimeSpan.FromSeconds(3));

try
{
await DoWorkWithCleanupAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}

リソース解放が必要な処理では、キャンセル対応と finally を必ずセットで考えましょう。

4. タイムアウトを実装する方法

4-1. CancelAfterで一定時間後に自動キャンセルする

CancelAfter() を使うと、指定した時間が経過した後に自動でキャンセル要求を出せます。

C#
using var cts = new CancellationTokenSource();

cts.CancelAfter(TimeSpan.FromSeconds(5));

try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("タイムアウトしました。");
}

CancelAfter は、指定したミリ秒数または TimeSpan が経過した後に、その CancellationTokenSource のキャンセル操作をスケジュールします。公式ドキュメントでも、CancelAfter(Int32)CancelAfter(TimeSpan) は指定時間後にキャンセル操作をスケジュールするメソッドとして説明されています。Microsoft Learn+2Microsoft Learn+2

タイムアウト実装としては、もっともシンプルで読みやすい方法です。

4-2. コンストラクタでタイムアウト時間を指定する

CancellationTokenSource のコンストラクタにタイムアウト時間を渡すこともできます。

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("5秒でタイムアウトしました。");
}

CancelAfter() と比べると、生成と同時にタイムアウトを開始したい場合に便利です。

C#
using var cts = new CancellationTokenSource(5000);

ミリ秒で指定することもできますが、可読性を考えると TimeSpan.FromSeconds() の方がおすすめです。

4-3. Task.DelayとWhenAnyを使ったタイムアウトとの違い

タイムアウトは Task.DelayTask.WhenAny でも実装できます。

C#
Task workTask = DoWorkAsync(CancellationToken.None);
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));

Task completedTask = await Task.WhenAny(workTask, timeoutTask);

if (completedTask == timeoutTask)
{
Console.WriteLine("タイムアウトしました。");
}
else
{
await workTask;
}

この方法は「指定時間内に終わったかどうか」を判定するには便利です。

ただし、このコードだけでは DoWorkAsync 自体は止まりません。タイムアウトした後も、裏側で処理が続く可能性があります。

処理そのものをキャンセルしたい場合は、CancellationTokenSource と組み合わせます。

C#
using var cts = new CancellationTokenSource();

Task workTask = DoWorkAsync(cts.Token);
Task timeoutTask = Task.Delay(TimeSpan.FromSeconds(5));

Task completedTask = await Task.WhenAny(workTask, timeoutTask);

if (completedTask == timeoutTask)
{
cts.Cancel();
Console.WriteLine("タイムアウトのためキャンセルしました。");
}

await workTask;

通常は、単純なタイムアウトなら CancelAfter()、複雑な待ち合わせ制御が必要なら Task.WhenAny() を使うと考えるとよいでしょう。

4-4. HttpClientリクエストをタイムアウトさせる実装例

HttpClient のリクエストにも CancellationToken を渡せます。HttpClient.SendAsync には、HttpRequestMessageHttpCompletionOptionCancellationToken を受け取るオーバーロードがあります。Microsoft Learn+2Microsoft Learn+2

C#
public async Task<string> GetJsonAsync(
HttpClient httpClient,
string url,
CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, url);

using var response = await httpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync(cancellationToken);
}

呼び出し側でタイムアウトを指定します。

C#
using var httpClient = new HttpClient();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

try
{
string json = await GetJsonAsync(
httpClient,
"https://example.com/api/items",
cts.Token);

Console.WriteLine(json);
}
catch (OperationCanceledException)
{
Console.WriteLine("HTTPリクエストがキャンセルまたはタイムアウトしました。");
}

HttpClient.Timeout プロパティもありますが、処理単位で柔軟に制御したい場合は CancellationTokenSource を使うと管理しやすくなります。

4-5. タイムアウトとユーザーキャンセルを区別する考え方

タイムアウトとユーザーキャンセルを区別したい場合は、CancellationTokenSource を分けて管理します。

C#
using var userCts = new CancellationTokenSource();
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userCts.Token,
timeoutCts.Token);

try
{
await DoWorkAsync(linkedCts.Token);
}
catch (OperationCanceledException)
{
if (timeoutCts.IsCancellationRequested)
{
Console.WriteLine("タイムアウトしました。");
}
else if (userCts.IsCancellationRequested)
{
Console.WriteLine("ユーザーによりキャンセルされました。");
}
else
{
Console.WriteLine("キャンセルされました。");
}
}

このように、実際の処理には連結したトークンを渡しつつ、原因判定には元の CancellationTokenSource を使います。

5. 複数のキャンセル条件を扱う応用テクニック

5-1. CreateLinkedTokenSourceで複数Tokenを連携する

複数のキャンセル条件を1つにまとめたい場合は、CancellationTokenSource.CreateLinkedTokenSource() を使います。

C#
using var userCts = new CancellationTokenSource();
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userCts.Token,
timeoutCts.Token);

await DoWorkAsync(linkedCts.Token);

CreateLinkedTokenSource で作成した CancellationTokenSource は、元になったいずれかのトークンがキャンセル状態になると、連動してキャンセル状態になります。公式ドキュメントでも、指定されたソーストークンのいずれかがキャンセル状態になったとき、作成された CancellationTokenSource もキャンセル状態になると説明されています。Microsoft Learn

これにより、次のような複数条件をまとめて扱えます。

  • ユーザーがキャンセルボタンを押した

  • タイムアウト時間を過ぎた

  • 親処理がキャンセルされた

  • ASP.NET Coreのリクエストが中断された

5-2. ユーザー操作とタイムアウトを同時に扱う例

ユーザー操作とタイムアウトを同時に扱う典型例です。

C#
private CancellationTokenSource? _userCts;

public async Task StartAsync()
{
_userCts = new CancellationTokenSource();

using var timeoutCts = new CancellationTokenSource(
TimeSpan.FromSeconds(15));

using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
_userCts.Token,
timeoutCts.Token);

try
{
await DoWorkAsync(linkedCts.Token);
Console.WriteLine("処理が完了しました。");
}
catch (OperationCanceledException)
{
if (_userCts.IsCancellationRequested)
{
Console.WriteLine("ユーザーによりキャンセルされました。");
}
else if (timeoutCts.IsCancellationRequested)
{
Console.WriteLine("タイムアウトしました。");
}
}
}

public void CancelByUser()
{
_userCts?.Cancel();
}

ポイントは、処理には linkedCts.Token を渡し、キャンセル原因は _userCtstimeoutCts の状態で判定することです。

5-3. 親子タスクでキャンセルを伝播させる方法

親処理から子処理へキャンセルを伝えたい場合も、同じ CancellationToken を渡します。

C#
public async Task ParentAsync(CancellationToken cancellationToken)
{
await ChildAAsync(cancellationToken);
await ChildBAsync(cancellationToken);
}

private async Task ChildAAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}

private async Task ChildBAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}

複数の子タスクを並列実行する場合も同様です。

C#
public async Task ParentParallelAsync(CancellationToken cancellationToken)
{
Task task1 = ChildAAsync(cancellationToken);
Task task2 = ChildBAsync(cancellationToken);

await Task.WhenAll(task1, task2);
}

親がキャンセルされたら、子処理も同じトークンでキャンセルを検知できます。

5-4. Registerでキャンセル時のコールバックを登録する

CancellationToken.Register() を使うと、キャンセルされたタイミングでコールバックを実行できます。

C#
using var cts = new CancellationTokenSource();

using CancellationTokenRegistration registration =
cts.Token.Register(() =>
{
Console.WriteLine("キャンセルが要求されました。");
});

cts.Cancel();

外部ライブラリや古いAPIなど、CancellationToken を直接受け取れない処理と連携する場合に便利です。

C#
using var cts = new CancellationTokenSource();

using var registration = cts.Token.Register(() =>
{
// キャンセル時に独自の中断処理を呼ぶ
legacyClient.Abort();
});

await ExecuteLegacyOperationAsync();

ただし、コールバック内で重い処理を実行するのは避けましょう。キャンセル通知の流れを複雑にし、デッドロックや予期しない遅延の原因になることがあります。

5-5. DisposeでCancellationTokenSourceを解放する理由

CancellationTokenSource は、内部的にタイマーや登録済みコールバックなどのリソースを持つ場合があります。

特に次のようなケースでは、Dispose() が重要です。

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
C#
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
token1,
token2);
C#
using var registration = token.Register(() =>
{
Console.WriteLine("キャンセルされました。");
});

使い終わった CancellationTokenSourceCancellationTokenRegistration は解放しましょう。CancellationTokenSource.Dispose() は、使い終わった CancellationTokenSource に対して呼び出すべきメソッドであり、呼び出し後はそのインスタンスを使用できません。Microsoft Learn

基本形は次のとおりです。

C#
using var cts = new CancellationTokenSource();

フィールドとして保持する場合は、不要になったタイミングで明示的に破棄します。

C#
private CancellationTokenSource? _cts;

public void Dispose()
{
_cts?.Dispose();
}

6. CancellationTokenSourceでよくあるエラーと注意点

6-1. Cancelしても処理が止まらない原因

Cancel() を呼んでも処理が止まらない原因の多くは、処理側が CancellationToken を見ていないことです。

悪い例です。

C#
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine(i);

// CancellationToken を渡していない
await Task.Delay(1000);
}
}

この場合、Cancel() を呼んでも Task.Delay はキャンセルされません。

修正例です。

C#
public async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine(i);

await Task.Delay(1000, cancellationToken);
}
}

キャンセル可能にするには、次の2つが重要です。

  • キャンセル対応APIに CancellationToken を渡す

  • 自前の処理では定期的にキャンセル状態を確認する

6-2. CancellationTokenを渡すだけではキャンセルされない理由

CancellationToken を渡すだけでは、すべての処理が自動的に止まるわけではありません。

たとえば次のコードでは、Task.RunCancellationToken を渡しています。

C#
await Task.Run(() =>
{
while (true)
{
// 無限ループ
}
}, cancellationToken);

このコードは、実行開始後にキャンセルしても止まりません。なぜなら、ループ内部で cancellationToken を確認していないからです。

正しくは次のように書きます。

C#
await Task.Run(() =>
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();

// 処理
}
}, cancellationToken);

CancellationToken は「確認されて初めて意味がある」仕組みです。

6-3. OperationCanceledExceptionとTaskCanceledExceptionの違い

キャンセル時には、OperationCanceledException または TaskCanceledException が発生することがあります。

OperationCanceledException は、キャンセルされた操作全般を表す例外です。

TaskCanceledException は、キャンセルされた Task を表す例外で、OperationCanceledException を継承しています。Microsoft公式ドキュメントでも、TaskCanceledException はキャンセルされたタスクに関する例外であり、CancellationToken などの情報は基底の OperationCanceledException から継承されることが示されています。Microsoft Learn

そのため、多くの場合は OperationCanceledException を捕捉すれば十分です。

C#
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}

TaskCanceledException だけを捕捉すると、他のキャンセル例外を取り逃がす可能性があります。

6-4. Dispose忘れによるリソースリーク

CancellationTokenSource は、タイムアウトやリンクトークン、コールバック登録を使う場合、内部リソースを保持します。

Disposeしない例です。

C#
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await DoWorkAsync(cts.Token);

推奨例です。

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
await DoWorkAsync(cts.Token);

リンクトークンを使う場合も同様です。

C#
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
token1,
token2);

特に、短時間で大量の CancellationTokenSource を作る処理では、Dispose忘れがパフォーマンスやリソース使用量に影響する可能性があります。

6-5. キャンセル済みCancellationTokenSourceの再利用はできるのか

キャンセル済みの CancellationTokenSource は、基本的に再利用しません。

C#
cts.Cancel();

// この cts を次の処理に使い回すのは避ける
await DoWorkAsync(cts.Token);

一度キャンセルされた CancellationTokenSourceToken は、キャンセル状態のままです。そのため、次の処理に渡すと即座にキャンセル扱いになります。

新しい処理には、新しい CancellationTokenSource を作成します。

C#
cts.Dispose();

cts = new CancellationTokenSource();
await DoWorkAsync(cts.Token);

ボタンクリックで開始・キャンセルを繰り返すUIでは、この再生成が特に重要です。

6-6. 例外を握りつぶしてはいけないケース

キャンセル処理では、OperationCanceledException を適切に扱う必要があります。

ただし、すべての例外を空の catch で握りつぶすのは危険です。

悪い例です。

C#
try
{
await DoWorkAsync(cts.Token);
}
catch
{
// 何もしない
}

この書き方では、キャンセルだけでなく、本当のバグや通信エラー、ファイルI/Oエラーも消えてしまいます。

良い例です。

C#
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}
catch (Exception ex)
{
Console.WriteLine($"エラーが発生しました: {ex.Message}");
throw;
}

キャンセルは想定内の制御フローとして扱い、それ以外の例外はログ出力や再スローなど、適切に処理しましょう。

7. 実践シーン別のCancellationTokenSource実装例

7-1. ボタンクリックで処理をキャンセルする例

デスクトップアプリやUIアプリでは、「開始」ボタンで処理を開始し、「キャンセル」ボタンで中断する実装がよくあります。

C#
private CancellationTokenSource? _cts;

private async void StartButton_Click(object sender, EventArgs e)
{
_cts?.Dispose();
_cts = new CancellationTokenSource();

try
{
await LongRunningOperationAsync(_cts.Token);
MessageBox.Show("完了しました。");
}
catch (OperationCanceledException)
{
MessageBox.Show("キャンセルされました。");
}
finally
{
_cts.Dispose();
_cts = null;
}
}

private void CancelButton_Click(object sender, EventArgs e)
{
_cts?.Cancel();
}

private async Task LongRunningOperationAsync(
CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();

await Task.Delay(100, cancellationToken);
}
}

ポイントは、開始するたびに新しい CancellationTokenSource を作ることです。キャンセル済みのものを再利用しないようにしましょう。

7-2. Web API呼び出しをキャンセルする例

HttpClient を使ったWeb API呼び出しでも、CancellationToken を渡します。

C#
public async Task<string> FetchDataAsync(
HttpClient httpClient,
CancellationToken cancellationToken)
{
using var response = await httpClient.GetAsync(
"https://example.com/api/data",
cancellationToken);

response.EnsureSuccessStatusCode();

return await response.Content.ReadAsStringAsync(cancellationToken);
}

呼び出し側でキャンセルできます。

C#
using var httpClient = new HttpClient();
using var cts = new CancellationTokenSource();

var task = FetchDataAsync(httpClient, cts.Token);

// 任意のタイミングでキャンセル
cts.Cancel();

try
{
string data = await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("API呼び出しをキャンセルしました。");
}

タイムアウトも組み合わせるなら次のようにします。

C#
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(10));

try
{
string data = await FetchDataAsync(httpClient, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("API呼び出しがタイムアウトしました。");
}

7-3. ファイル読み込み・書き込み処理をキャンセルする例

ファイル処理でも、キャンセル対応の非同期APIを使うと安全に中断できます。

C#
public async Task<string> ReadFileAsync(
string path,
CancellationToken cancellationToken)
{
return await File.ReadAllTextAsync(path, cancellationToken);
}

書き込み処理の例です。

C#
public async Task WriteFileAsync(
string path,
string content,
CancellationToken cancellationToken)
{
await File.WriteAllTextAsync(path, content, cancellationToken);
}

大きなファイルを分割して処理する場合は、ループ内でもキャンセルを確認します。

C#
public async Task CopyFileAsync(
string sourcePath,
string destinationPath,
CancellationToken cancellationToken)
{
await using var source = File.OpenRead(sourcePath);
await using var destination = File.Create(destinationPath);

var buffer = new byte[81920];

while (true)
{
cancellationToken.ThrowIfCancellationRequested();

int bytesRead = await source.ReadAsync(
buffer.AsMemory(0, buffer.Length),
cancellationToken);

if (bytesRead == 0)
{
break;
}

await destination.WriteAsync(
buffer.AsMemory(0, bytesRead),
cancellationToken);
}
}

長時間のI/O処理では、APIにトークンを渡すことと、自前ループでチェックすることの両方が重要です。

7-4. バックグラウンド処理をキャンセルする例

バックグラウンドで定期実行する処理にも CancellationTokenSource は有効です。

C#
private CancellationTokenSource? _cts;
private Task? _backgroundTask;

public void StartBackgroundWork()
{
_cts = new CancellationTokenSource();

_backgroundTask = Task.Run(
() => BackgroundLoopAsync(_cts.Token));
}

public async Task StopBackgroundWorkAsync()
{
if (_cts is null || _backgroundTask is null)
{
return;
}

_cts.Cancel();

try
{
await _backgroundTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("バックグラウンド処理を停止しました。");
}
finally
{
_cts.Dispose();
_cts = null;
_backgroundTask = null;
}
}

private async Task BackgroundLoopAsync(
CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();

Console.WriteLine("バックグラウンド処理を実行中");

await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}

while (true) のような常駐処理では、キャンセルチェックを入れないと正常終了できません。

7-5. ASP.NET Coreでリクエスト中断を検知する例

ASP.NET Coreでは、クライアントが接続を切断した場合などに HttpContext.RequestAborted でキャンセルを検知できます。公式ドキュメントでも、HttpContext.RequestAborted はHTTPリクエストがクライアントまたはサーバーによって中止されたことを通知するキャンセルトークンであり、長時間実行タスクへ渡すことが推奨されています。Microsoft Learn+2Microsoft Learn+2

Minimal APIの例です。

C#
app.MapGet("/reports", async (
HttpContext context,
ReportService reportService) =>
{
CancellationToken cancellationToken = context.RequestAborted;

var report = await reportService.CreateReportAsync(cancellationToken);

return Results.Ok(report);
});

Controllerでは、アクション引数に CancellationToken を受け取る形もよく使われます。

C#
[HttpGet("reports")]
public async Task<IActionResult> GetReport(
CancellationToken cancellationToken)
{
var report = await _reportService.CreateReportAsync(cancellationToken);

return Ok(report);
}

サービス側もトークンを受け取ります。

C#
public async Task<Report> CreateReportAsync(
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

await Task.Delay(3000, cancellationToken);

return new Report();
}

ASP.NET Coreでは、リクエストが中断された後もサーバー側で不要な処理を続けないよう、DBアクセス、外部API呼び出し、ファイル処理などに CancellationToken を渡す設計が重要です。

8. CancellationTokenSourceのベストプラクティス

8-1. CancellationTokenはメソッド引数で受け取る

キャンセル可能なメソッドは、CancellationToken を引数で受け取るようにします。

C#
public async Task DoWorkAsync(
CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}

省略可能にする場合は default を指定します。

C#
public async Task DoWorkAsync(
CancellationToken cancellationToken = default)
{
await Task.Delay(1000, cancellationToken);
}

呼び出し元にキャンセルの主導権を渡すため、メソッド内部でむやみに CancellationTokenSource を作らないことが大切です。

8-2. キャンセル可能な処理には定期的にチェックを入れる

長時間処理では、定期的にキャンセル状態を確認します。

C#
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();

await ProcessItemAsync(item, cancellationToken);
}

特に以下のような処理ではチェックが必要です。

  • 長いループ

  • 大量データ処理

  • CPU負荷の高い計算

  • 複数ファイルの読み書き

  • 外部APIの連続呼び出し

  • バックグラウンド常駐処理

チェック間隔が長すぎると、キャンセルしてから実際に止まるまで時間がかかります。

8-3. ライブラリ側では勝手にCancellationTokenSourceを作らない

ライブラリや共通メソッド側では、基本的に CancellationTokenSource を勝手に作るべきではありません。

避けたい例です。

C#
public async Task DoWorkAsync()
{
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(10));

await Task.Delay(1000, cts.Token);
}

この書き方では、呼び出し元がキャンセル制御できません。

推奨例です。

C#
public async Task DoWorkAsync(
CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}

タイムアウトを設定したい場合も、基本的には呼び出し元で CancellationTokenSource を作ります。

C#
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(10));

await service.DoWorkAsync(cts.Token);

コンポーネントが自分で CancellationTokenSource を作成した場合は、そのコンポーネント自身が破棄し、呼び出し元から渡されたトークンは破棄しない、という考え方も重要です。Microsoftの.NET非同期プログラミング関連ドキュメントでも、コンポーネントが作成した CancellationTokenSource はそのコンポーネントが破棄し、呼び出し元が渡したトークンは破棄しないという考え方が示されています。Microsoft Learn

8-4. finallyでリソース解放を確実に行う

キャンセルされても、finally で後処理を実行します。

C#
public async Task ExecuteAsync(
CancellationToken cancellationToken)
{
var resource = new SomeResource();

try
{
await resource.OpenAsync(cancellationToken);
await resource.ProcessAsync(cancellationToken);
}
finally
{
await resource.DisposeAsync();
}
}

usingawait using を使える場合は、さらに安全です。

C#
public async Task ExecuteAsync(
CancellationToken cancellationToken)
{
await using var stream = File.OpenRead("data.txt");

var buffer = new byte[1024];

int read = await stream.ReadAsync(
buffer.AsMemory(0, buffer.Length),
cancellationToken);
}

キャンセルは任意のタイミングで発生し得るため、途中で処理が止まってもリソースが残らない設計にしましょう。

8-5. タイムアウト値はハードコードしすぎない

タイムアウト値をコード内に直接書きすぎると、後から調整しにくくなります。

避けたい例です。

C#
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(3));

設定値として外に出すと、環境ごとの調整がしやすくなります。

C#
public class ApiOptions
{
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
}
C#
using var cts = new CancellationTokenSource(options.Timeout);

Web API、バッチ処理、ファイル処理などでは、適切なタイムアウト値が環境によって異なります。固定値ではなく、設定ファイルやオプションから渡せるようにしておくと運用しやすくなります。

8-6. キャンセルは例外ではなく正常な制御フローとして扱う

OperationCanceledException は例外として発生しますが、意味としては「想定内のキャンセル」です。

そのため、ログレベルをエラーにしない、アラート対象にしない、ユーザーには自然なメッセージを表示する、などの配慮が必要です。

C#
try
{
await DoWorkAsync(cancellationToken);
}
catch (OperationCanceledException)
{
logger.LogInformation("処理はキャンセルされました。");
}
catch (Exception ex)
{
logger.LogError(ex, "処理中にエラーが発生しました。");
throw;
}

キャンセルと障害を区別することで、ログのノイズを減らし、本当に対応が必要なエラーを見つけやすくなります。

9. CancellationTokenSourceに関するよくある質問

9-1. CancellationTokenSourceとCancellationTokenはどちらを渡すべき?

メソッドに渡すべきなのは、基本的に CancellationToken です。

C#
public async Task DoWorkAsync(
CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken);
}

CancellationTokenSource はキャンセルを発行する側が持ちます。

C#
using var cts = new CancellationTokenSource();

await DoWorkAsync(cts.Token);

処理側に CancellationTokenSource を渡してしまうと、処理側が勝手に Cancel() を呼べてしまいます。責務を分離するためにも、渡すのは CancellationToken にしましょう。

9-2. CancelAfterとTask.Delayはどちらを使うべき?

処理そのものをキャンセルしたいなら、CancelAfter() がシンプルです。

C#
using var cts = new CancellationTokenSource();

cts.CancelAfter(TimeSpan.FromSeconds(10));

await DoWorkAsync(cts.Token);

一方、Task.DelayTask.WhenAny は「どちらが先に終わったか」を判定したい場合に向いています。

C#
Task completed = await Task.WhenAny(
DoWorkAsync(cancellationToken),
Task.Delay(TimeSpan.FromSeconds(10)));

ただし、Task.Delay だけでは元の処理は止まりません。タイムアウト時に処理を止めたい場合は、結局 CancellationTokenSource と組み合わせる必要があります。

9-3. Cancelを呼ぶとTaskは即座に停止する?

即座に停止するとは限りません。

Cancel() はキャンセル要求を通知するだけです。処理側が CancellationToken を確認して初めてキャンセルされます。

C#
cts.Cancel();

処理側では次のような確認が必要です。

C#
cancellationToken.ThrowIfCancellationRequested();

または、キャンセル対応APIにトークンを渡します。

C#
await Task.Delay(1000, cancellationToken);

C#のキャンセルは強制終了ではなく、協調的キャンセルです。

9-4. キャンセル後に同じCancellationTokenSourceを再利用できる?

基本的に再利用しません。

一度キャンセルされた CancellationTokenSource はキャンセル状態のままです。その Token を次の処理に渡すと、最初からキャンセル済みとして扱われます。

C#
cts.Cancel();

// 次の処理には使い回さない
await DoWorkAsync(cts.Token);

新しい処理を開始する場合は、新しい CancellationTokenSource を作ります。

C#
cts.Dispose();
cts = new CancellationTokenSource();

9-5. await中の処理はどうやってキャンセルされる?

await 中の処理がキャンセルに対応している場合、渡された CancellationToken のキャンセル要求を検知し、OperationCanceledException を発生させます。

C#
await Task.Delay(10000, cancellationToken);

この状態で Cancel() が呼ばれると、Task.Delay はキャンセルされ、await している箇所で OperationCanceledException が発生します。

C#
try
{
await Task.Delay(10000, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("await中にキャンセルされました。");
}

ただし、awaitしている処理が CancellationToken に対応していない場合は、自動的にはキャンセルされません。

9-6. ConfigureAwaitとCancellationTokenは関係ある?

ConfigureAwaitCancellationToken は別の仕組みです。

ConfigureAwait(false) は、await 後に元の同期コンテキストへ戻るかどうかを制御します。

C#
await SomeAsync().ConfigureAwait(false);

一方、CancellationToken は処理をキャンセル可能にするための仕組みです。

C#
await SomeAsync(cancellationToken);

つまり、目的が違います。

両方を同時に使うことはあります。

C#
await SomeAsync(cancellationToken)
.ConfigureAwait(false);

ライブラリコードでは ConfigureAwait(false) を使うことがありますが、キャンセル制御とは独立して考えましょう。

まとめ

CancellationTokenSource は、C#で非同期処理や長時間処理を安全にキャンセルするための重要な仕組みです。

基本は、呼び出し元で CancellationTokenSource を作成し、処理側には CancellationToken を渡します。

C#
using var cts = new CancellationTokenSource();

await DoWorkAsync(cts.Token);

キャンセルしたいタイミングで Cancel() を呼びます。

C#
cts.Cancel();

タイムアウトを実装したい場合は、CancelAfter() またはタイムアウト付きコンストラクタを使います。

C#
using var cts = new CancellationTokenSource(
TimeSpan.FromSeconds(10));

複数のキャンセル条件を扱う場合は、CreateLinkedTokenSource() が便利です。

C#
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userToken,
timeoutToken);

ただし、Cancel() を呼んだからといって処理が強制停止されるわけではありません。処理側で CancellationToken を受け取り、ThrowIfCancellationRequested() やキャンセル対応APIを使って、定期的にキャンセルを確認する必要があります。

C#
cancellationToken.ThrowIfCancellationRequested();

また、CancellationTokenSource は使い終わったら Dispose() する、キャンセル済みのインスタンスを再利用しない、OperationCanceledException を通常のエラーと分けて扱う、といった点も重要です。

C#のキャンセル制御は、強制終了ではなく協調的キャンセルです。CancellationTokenSourceCancellationToken の役割を正しく理解し、非同期処理、タイムアウト、ユーザー操作、Web API、ASP.NET Coreなどの実装に組み込むことで、応答性が高く安全なアプリケーションを作ることができます。