【Unity】分かりやすいUniTaskのキャンセル方法まとめ!

UniTaskのキャンセルする方法のまとめです。

UniTaskなどの非同期処理はとても便利ですが、キャンセルが絡むととってもややこしくなります。

ちゃんと理解しないと思わぬバグにつながることもあるので、少しややこしいですが頑張って使いこなしましょう!

CancellationTokenSourceの作成

UniTaskをキャンセルする場合、CancellationTokenSourceを作成する必要があります。

最終的にCancellationTokenSourceのCancel関数を実行するとTaskをキャンセルできます。

var cts = new CancellationTokenSource();
cts.Cancel();

キャンセルさせたい処理にTokenを設定する

CancellationTokenSourceをキャンセルするだけでは何も起きません。

CancellationTokenSourceが持つToken使って処理がキャンセルされるように、事前設定しておく必要があります。

設定の種類としては以下のものがあります。

  • 非同期処理の引数にtokenを設定する
  • ThrowIfCancellationRequested関数を実行する
  • IsCancellationRequestedをチェックする

非同期処理の引数にtokenを設定する

UniTaskの非同期関数の引数にはcancellationTokenを設定する引数が用意されているので、そこにTokenを設定します。

async UniTask SampleTask()
{
    var token = m_cts.Token;
    //処理1
    //10秒待つ。待っている途中でキャンセルされた場合、タスク終了。
    await UniTask.WaitForSeconds(10, cancellationToken: token);
    //処理2
}

処理1の途中でcts.Cancel()が発生すると、処理が実行中の場合は中断されSampleTaskが終了します。

リソースのロードなどのAsyncOperation系の非同期処理にはcancellationTokenを設定する引数がありませんが、WithCancellationという拡張メソッドがあるので、こちらを使います。

Addressables.LoadAssetAsync<Sprite>(path).WithCancellation(cts.Token);

ThrowIfCancellationRequested関数を実行する

cts.TokenのThrowIfCancellationRequested関数を実行すると、その時点でcts.Cancel()が発生していた場合タスク終了します。

async UniTask SampleTask()
{
    var token = m_cts.Token;
    //処理1
    //キャンセルされていた場合、タスク終了
    token.ThrowIfCancellationRequested();
    //処理2
}

処理1の途中でcts.Cancel()が発生すると、処理2は実行されずSampleTaskが終了します。

IsCancellationRequestedをチェックする

cts.TokenのプロパティIsCancellationRequestedは、その時点でcts.Cancel()が発生していた場合trueを返します。

async UniTask SampleTask()
{
    var token = m_cts.Token;
    //処理1
    //この段階でキャンセルされていた場合trueに
    if (token.IsCancellationRequested)
    {
        //キャンセル処理など
        return;
    }
    //処理2
}

ThrowIfCancellationRequestedと似ていますが、こちらの場合キャンセル時に好きな処理を入れることができます。

キャンセル例外のハンドリング方法

先ほど「cts.Cancel()が発生すると、処理は中断される」と説明しましたが、厳密には例外(OperationCanceledException)が発生しています。

Taskを呼び出した大本で例外をキャッチする処理が書かれていないとキャンセルが起きた際にエラーとなり、それ以降の処理が実行されません

下記のコードを実行すると、OperationCanceledExceptionがキャッチされずエラーが発生し「タスク終了後の処理」は通りません。

private async void Start()
{
    m_cts = new CancellationTokenSource();
    var ct = m_cts.Token;
    //5秒後にキャンセル
    Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ => m_cts?.Cancel());

    await SampleTask(ct);

    //タスク終了後の処理
}

async UniTask SampleTask(CancellationToken ct)
{
    //10秒待つ
    await UniTask.WaitForSeconds(10, cancellationToken: ct);
}

これを回避するためには例外をキャッチしてエラーを出さないようにしてやる必要があります。

方法としては次の3つあります。

  • Try-Catchを使う
  • SuppressCancellationThrow関数を使う
  • Forget関数を使う

Try-Catchを使う

Try-Catchはエラーとなる例外をキャッチできる構文です。(詳しくは調べてください)

OperationCanceledExceptionをキャッチしておきます。

キャンセルされた時にしたい処理がなければ、キャッチだけして無視しておけば良いです。

private async void Start()
{
    m_cts = new CancellationTokenSource();
    var ct = m_cts.Token;
    //5秒後にキャンセル
    Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ => m_cts?.Cancel());

    try
    {
        await SampleTask(ct);
    }
    catch(OperationCanceledException e){}

    //タスク終了後の処理
}

async UniTask SampleTask(CancellationToken ct)
{
    //10秒待つ
    await UniTask.WaitForSeconds(10, cancellationToken: ct);
}

SuppressCancellationThrow関数を使う

SuppressCancellationThrowは実行したTaskがキャンセルされたかどうか、を返してくれる関数です。

この中で例外の処理も行ってくれているのでTry-Catchと同じ使い方ができます。

1行で済むのでTry-Catchを使うより便利ですね。

private async void Start()
{
    m_cts = new CancellationTokenSource();
    var ct = m_cts.Token;
    //5秒後にキャンセル
    Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ => m_cts?.Cancel());

    bool result = await SampleTask(ct).SuppressCancellationThrow();

    //タスク終了後の処理
}

async UniTask SampleTask(CancellationToken ct)
{
    //10秒待つ
    await UniTask.WaitForSeconds(10, cancellationToken: ct);
}

Forget関数を使う

上2つはawaitでタスクの完了を待ちますが、Forgetはタスクを待たない場合に使います。

こちらも中で例外を処理していくれています。

private async void Start()
{
    m_cts = new CancellationTokenSource();
    var ct = m_cts.Token;
    //5秒後にキャンセル
    Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ => m_cts?.Cancel());

    SampleTask(ct).Forget();

    //タスク終了後の処理
}

async UniTask SampleTask(CancellationToken ct)
{
    //10秒待つ
    await UniTask.WaitForSeconds(10, cancellationToken: ct);
}

キャンセル(OperationCanceledException)が発生したときの処理の挙動はどうなるか知っているでしょうか?

答えは「直近のハンドリング処理に飛ぶ」でした。

async UniTask SampleTask1(CancellationToken ct)
{
    await SampleTask2(ct).SuppressCancellationThrow();//①
    //処理1
}
async UniTask SampleTask2(CancellationToken ct)
{
    await SampleTask3(ct).SuppressCancellationThrow();//②
    //処理2
}
async UniTask SampleTask3(CancellationToken ct)
{
    //10秒待つ。途中でキャンセル発生!!
    await UniTask.WaitForSeconds(10, cancellationToken: ct);
}

このコードの場合、キャンセルが発生した際に行く先は②です。

そのためキャンセル後に「処理2」「処理1」が実行されます。

基本的にはタスクを呼び出した大本でのみ例外のハンドリングをしておけばOKです!

まとめ

今回はUniTaskのキャンセル方法を紹介しました。

しっかりマスターして安全にUniTaskを使いましょう!

コメント