こんにちは、エーティーエルシステムズ 鍋島です。
C#のTask(非同期プログラミング)周りの理解が浅いので、一度整理するために実験的なサンプルコードを用意しました。
実験的なコードサンプル
次のサンプルコードでは次の3つのTask(task1,task2,task3)を作成して、3つのすべての非同期のタスクが完了するまで待機します。最後に、それぞれのタスクの状態を表示します。
- 正常に完了するタスク (task1)
Thread.Sleep(500)でシミュレーションし、その後正常に完了します - 例外をスローするタスク (task2)
InvalidOperationExceptionをスローし、失敗します - キャンセルされるタスク (task3)
CancellationTokenSourceを使用してタスクを途中でキャンセルします
namespace TaskSpike;
using System;
using System.Threading;
using System.Threading.Tasks;
class TaskSpike
{
/// <summary>
/// 説明:
/// 正常に完了するタスク (task1)
/// Thread.Sleep(500)でシミュレーションし、その後正常に完了します。
/// 例外をスローするタスク (task2)
/// InvalidOperationExceptionをスローし、失敗(IsFaulted)します。
/// キャンセルされるタスク (task3)
/// CancellationTokenSourceを使用してタスクを途中でキャンセルします。
/// 出力例:
///current:0 1 2
///[200ms経過] Task 3 was canceled.
///[500ms経過] Task 2 failed
///[500ms経過] Task 1 completed successfully.
///Exception in one of the tasks: Task 2 failed.
///[CheckTaskStatus]: Task 1 completed successfully.
///[CheckTaskStatus]: Task 2 failed: One or more errors occurred. (Task 2 failed.)
///[CheckTaskStatus]: Task 3 was canceled! </summary>
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
static async Task Main(string[] args)
{
// タスク1: 正常に完了するタスク
var task1 = Task.Run(async () =>
{
// 何かの処理をシミュレート
await Task.Delay(500);
Console.WriteLine($"[500ms経過] Task 1 completed successfully. ");
});
// タスク2: 例外をスローするタスク
var task2 = Task.Run(async () =>
{
// 例外をスロー
await Task.Delay(500);
Console.WriteLine($"[500ms経過] Task 2 failed");
throw new InvalidOperationException("Task 2 failed.");
});
// タスク3: キャンセルされるタスク
using var cts = new CancellationTokenSource();
var task3 = Task.Run(async () =>
{
// タスクがキャンセルされるまで処理を待機
Console.Write("current:");
for (int i = 0; i < 10; i++)
{
try{
Console.Write($"{i} ");
await Task.Delay(100,cts.Token); //<--キャンセルトークンを渡す
}catch{
Console.WriteLine($"\n[{100 * i}ms経過] Task 3 was canceled.");
//cts.Token.ThrowIfCancellationRequested(); //OperationCanceledException を返していたので、変更
throw; //引数なしでthrow(リスロー)すると、そのままTaskCanceledExceptionを返す。
}
}
}, cts.Token);
// キャンセルのタイミングは250ms後
cts.CancelAfter(250);
// すべてのタスクを待機し、結果を確認
try
{
// すべてのタスクを待機し、結果を確認
await Task.WhenAll(task1, task2, task3);
}
catch (Exception ex)
{
// Task.WhenAll の呼び出し中に例外が発生した場合でも、チェックは続ける
Console.WriteLine($"Exception in one of the tasks: {ex.Message}");
}
// タスクの状態をチェック
CheckTaskStatus(task1, "Task 1");
CheckTaskStatus(task2, "Task 2");
CheckTaskStatus(task3, "Task 3");
}
/// <summary>
/// タスクの状態をチェックして、結果をコンソールに出力するメソッドです。
/// </summary>
/// <param name="task">状態を確認する対象のタスク</param>
/// <param name="taskName">タスク名。コンソール出力に表示するために使用</param>
static void CheckTaskStatus(Task task, string taskName)
{
//if (task.IsCompleted) //IsCompletedだとIsFaultedもIsCanceledもIsCompletedにヒットする。
if (task.IsCompletedSuccessfully) //
{
Console.WriteLine($"[CheckTaskStatus]: {taskName} completed successfully.");
}
if (task.IsFaulted)
{
Console.WriteLine($"[CheckTaskStatus]: {taskName} failed: {task.Exception?.Message}");
}
if (task.IsCanceled)
{
Console.WriteLine($"[CheckTaskStatus]: {taskName} was canceled!");
}
}
}
出力例
上記のコードを出力すると次のように出力されます。
current:0 1 2 3
[300ms経過] Task 3 was canceled.
[500ms経過] Task 2 failed
[500ms経過] Task 1 completed successfully.
Exception in one of the tasks: Task 2 failed.
[CheckTaskStatus]: Task 1 completed successfully.
[CheckTaskStatus]: Task 2 failed: One or more errors occurred. (Task 2 failed.)
[CheckTaskStatus]: Task 3 was canceled!
まとめ
サンプルコードを書きながらつまずいたのは次の3点。特にIsCompleted と IsCompletedSuccessfullyは、うっかり間違いそうなので気を付けよう。
-
WhenAll
を最初TryCatchで例外処理していなかったので、Exceptionがスローされ、後続のCheckTaskStatusは呼び出されなかった。 -
task3のタスクをキャンセルするときにトークンに対してThrowIfCancellationRequestedを実行しないと、task3が成功扱いになってしまった。
変更前:Return;
変更後:cts.Token.ThrowIfCancellationRequested();
(1/8追記)コメントでご指摘いただき、Thread.SleepからTask.Delayに変更したタイミングで、Task.DelayでCancellationTokenを使うコードに変更し、上記のコードは不要となりました。 -
CheckTaskStatusメソッドの中でタスクが成功したかうまく判定できなかった。
変更前:if (task.IsCompleted)
変更後:if (task.IsCompletedSuccessfully)
IsCompletedはタスクが3つの最終状態(RanToCompletion、Faulted、またはCanceled)のいずれかにある場合にtrueとなってしまうので注意
追記(2025/01/08)
もとの投降のコードから2点変更しました。いずれも @juner さんコメントでのご指摘ありがとうございました。
- Thread.Sleepを使っていましたが、Task.Delay を使うように変更しました。特にtask3では引数にキャンセルトークンを渡しています。
await Task.Delay(100,cts.Token);
Thread.SleepとTask.Delayの違いがまだ完全には理解しきれていないのですが、Thread.Sleepはメインと同じスレッドごとを停止してしまうことがあると。
@TsuyoshiUshioさんの投稿を参考にしました。
- CancellationTokenSourceでusing 使うようにしました。明示的にdisposeしないとメモリリークの原因になるようです
using var cts = new CancellationTokenSource();
まだ、実験コードに誤りがあるようでしたら、ドシドシご指摘ください!