TaskをCancellationTokenSourceを使ってキャンセルする。

こんにちは、エーティーエルシステムズ 鍋島です。
C#のTask(非同期プログラミング)周りの理解が浅いので、一度整理するために実験的なサンプルコードを用意しました。

# 実験的なコードサンプル
次のサンプルコードでは次の３つのTask（task1,task2,task3）を作成して、3つのすべての非同期のタスクが完了するまで待機します。最後に、それぞれのタスクの状態を表示します。

* 正常に完了するタスク (task1)
  Thread.Sleep(500)でシミュレーションし、その後正常に完了します
* 例外をスローするタスク (task2)
  InvalidOperationExceptionをスローし、失敗します
* キャンセルされるタスク (task3)
   CancellationTokenSourceを使用してタスクを途中でキャンセルします

```cs
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は、うっかり間違いそうなので気を付けよう。

1. ```WhenAll```を最初TryCatchで例外処理していなかったので、Exceptionがスローされ、後続のCheckTaskStatusは呼び出されなかった。

1. ~~task3のタスクをキャンセルするときにトークンに対してThrowIfCancellationRequestedを実行しないと、task3が成功扱いになってしまった。
   <b>変更前</b>：```Return;```
   <b>変更後</b>：```cts.Token.ThrowIfCancellationRequested();```~~
   (1/8追記)コメントでご指摘いただき、Thread.SleepからTask.Delayに変更したタイミングで、Task.DelayでCancellationTokenを使うコードに変更し、上記のコードは不要となりました。  

1. CheckTaskStatusメソッドの中でタスクが成功したかうまく判定できなかった。
   <b>変更前</b>:```if (task.IsCompleted)```
   <b>変更後</b>:```if (task.IsCompletedSuccessfully)```
   IsCompletedはタスクが3つの最終状態（RanToCompletion、Faulted、またはCanceled）のいずれかにある場合にtrueとなってしまうので注意

## 追記(2025/01/08)
もとの投降のコードから2点変更しました。いずれも @juner さんコメントでのご指摘ありがとうございました。

1. Thread.Sleepを使っていましたが、Task.Delay を使うように変更しました。特にtask3では引数にキャンセルトークンを渡しています。
　　```await Task.Delay(100,cts.Token); ```
   Thread.SleepとTask.Delayの違いがまだ完全には理解しきれていないのですが、Thread.Sleepはメインと同じスレッドごとを停止してしまうことがあると。
 @TsuyoshiUshioさんの投稿を参考にしました。

  https://qiita.com/TsuyoshiUshio@github/items/e9404651c9e48f1b8443

1. CancellationTokenSourceでusing 使うようにしました。明示的にdisposeしないとメモリリークの原因になるようです
  ```using var cts = new CancellationTokenSource();```

  まだ、実験コードに誤りがあるようでしたら、ドシドシご指摘ください！