【C#】Taskのキャンセル

C# 標準の Task のキャンセルの方法です。

以下のように bool のフラグを使ってキャンセルするのは方法が簡単ですが、Task が想定するキャンセルの作法をの紹介になります。

簡易キャンセル

フラグ使うほう。以下条件の時は別にこれでも大丈夫です。

  • 他人に実装を公開する予定が無い
  • 同時実行するタスクが無いもしくは少ない数

フラグを使用する場合でも、タスクの最低限のお作法として、キャンセルしたときは OperationCanceledException を投げます。

// 簡単なTaskのキャンセル
bool isCancel;
Task _task;

public void Run()
{
    _task = Task.Run(() =>
    {
        while(true) // キャンセルフラグが立ったら例外を投げる(例外投げるのはTaskのお約束
        {
            if(isCancel) throw new OperationCanceledException("canceled");
            System.Thread.Sleep(1000/*sec*/);
        }
    }); // 例外はハンドルしない
}

Taskのキャンセル

以下正しい Task のキャンセルの書き方です。

簡易版の条件に該当しない場合こちらを使用します。以下に該当する場合こちらの書き方をしたほうが無難です。

  • 他人にライブラリを公開するかもしれない
  • 沢山タスクを起動することがあってその中でキャンセルする可能性がある
  • 複数種類のタスクを同時に実行する

標準ライブラリなどを使用する場合このお作法を要求されます。

using System.Threading.Tasks;

internal class Program
{
    // キャンセル用のオブジェクト
    static CancellationTokenSource _cs = new();

    private static Task Main(string[] args)
    {
        Foo(_cs.Token); // Fire & Forgetパターンで放り投げて終わり

        while (true)
        {
            Console.Write(">");
            string input = Console.ReadLine();
            if (string.Compare(input.Trim(), "cancel", true) == 0)
            {
                _cs.Cancel(); // キャンセルの送信
            }
        }
    }

    private static async void Foo(CancellationToken ct)
    {
        await Task.Run(() =>
        {
            for (int i = 0; i < 100; i++)
            {
                // 検出されたかどうかの判定
                if (ct.IsCancellationRequested)
                {
                    throw new OperationCanceledException("キャンセルを検出しました。");
                }

                // キャンセルされてたら OperationCanceledException を投げるメソッド
                ct.ThrowIfCancellationRequested();

                Trace.WriteLine($"{DateTime.Now:HH:mm:ss} [{i}]");
                Task.Delay(1000).Wait();
            }
        }
        , ct);
    }

上位側で CancellationToken.Cancel() を呼び出してもタスクが自動で停止したり、例外が勝手に発生したりはしません。処理中で自分で IsCancellationRequested を見て判定したり、ThrowIfCancellationRequested を呼び出す必要があります。

CancellationToken の IsCancellationRequested を参照することで Task 内でキャンセルを検出することが可能です。検出したいタイミングでプロパティを都度確認します。キャンセルされたのを検出したら例外を投げる場合 ThrowIfCancellationRequested メソッドを使用すると OperationCanceledException が発生します。

タスクの処理が解された後、キャンセル終了した事を知るためには OperationCanceledException を throw して終了します。例外を投げて終了した場合は、Task.Status プロパティに Canceled が設定されます。タスクの中身が開始される前に TokenSource からキャンセルすると処理は開始されずに Canceled になります。タスクの処理が開始された後にキャンセル例外を握りつぶしたり return 終了すると RanToCompletion となり正常終了扱いされます。

これ、かなり書くのが面倒ですが、外部ライブラリの async 処理をキャンセルする時に必要になるため覚えておきましょう。複数タスクが起動しているときに TokenSource 経由のキャンセルは個々に指示出す必要が無くて便利だったりします。

Unityの場合

Unity の場合、Unitask を別途導入する必要がありますが、よりパフォーマンスに優れた UniTask.Run を使用することができます。

Task.Run(... を UniTask.Run(... に変更する + 戻り値がある場合 Task から UniTask に変更することで対応できます。扱い方は完全に同じです。