1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-08

こんにちは、エーティーエルシステムズ 鍋島です。
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は、うっかり間違いそうなので気を付けよう。

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

  2. task3のタスクをキャンセルするときにトークンに対してThrowIfCancellationRequestedを実行しないと、task3が成功扱いになってしまった。
    変更前Return;
    変更後cts.Token.ThrowIfCancellationRequested();

    (1/8追記)コメントでご指摘いただき、Thread.SleepからTask.Delayに変更したタイミングで、Task.DelayでCancellationTokenを使うコードに変更し、上記のコードは不要となりました。

  3. CheckTaskStatusメソッドの中でタスクが成功したかうまく判定できなかった。
    変更前:if (task.IsCompleted)
    変更後: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さんの投稿を参考にしました。

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

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

1
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?