LoginSignup
9
7

UniTaskの cancelImmediately フラグについて

Last updated at Posted at 2023-12-13

はじめに

こちらの記事はUnity Advent Calendar 2023 14日目の記事です。
UniTask Ver.2.5.0で新しく追加された機能 cancelImmediately について解説しようと思います

記事の内容のサンプルはGitHubに上げてあります(https://github.com/euglenach/UniTaskCancelImmediately)

cancelImmediately

cancelImmediatelyフラグは、その名の通りCancellationTokenがキャンセルになったとき、タスクを即座にキャンセル状態にするか(キャンセル例外が吐かれるか)を指定できるオプションです。
UniTask構造体やUniTaskAsyncEnumerableクラスで宣言されているファクトリメソッドや拡張メソッドの引数に追加されました
デフォルトではfalseです

じゃあ今まではすぐにキャンセルにならなかったこと?」という疑問が湧くと思いますが、実はその通りです。以下の例を見てください

private readonly CancellationTokenSource cts = new();

private async UniTaskVoid Start()
{
    Debug.Log($"DelayAfterFrame:{Time.frameCount}");
    try
    {
        await UniTask.Delay(1000, cancellationToken: cts.Token);
    } catch(OperationCanceledException)
    {
        Debug.Log($"CanceledFrame:{Time.frameCount}");
    }
}

private void LateUpdate()
{
    cts.Cancel();
}

UniTask.Delay にキャンセル例外を吐かせ、前後のフレームを見るというシンプルなものです

a.png

結果は、catch句に入った時点では1フレーム遅れていることがわかります

UniTask.Delay毎フレーム タスクが完了・キャンセルされているかを確認しています
その確認の後にキャンセルされていたら、同フレームであっても例外が吐かれるのはその次のフレームの確認の際になります

c.png

余談ですが、確認のタイミングは引数の delayTimingオプション で指定できます

await UniTask.Delay(1000, delayTiming: PlayerLoopTiming.FixedUpdate);

さて、ここからが本題です

cancelImmediately をtrueにして確認してみましょう

private readonly CancellationTokenSource cts = new();
private async UniTaskVoid Start()
{
    Debug.Log($"DelayAfterFrame:{Time.frameCount}");
    try
    {
        // cancelImmediatelyオプションを設定する
        await UniTask.Delay(1000, cancellationToken: cts.Token, cancelImmediately: true);
    } catch(OperationCanceledException)
    {
        Debug.Log($"CanceledFrame:{Time.frameCount}");
    }
}
private void LateUpdate()
{
    cts.Cancel();
}

b.jpg

キャンセルした同フレームでcatch句に入っていることがわかります
cancelImmediatelyオプションを true にすると、即座に例外が吐かれるようになります

d.png

該当の実装を見ると、CancellationToken.Register でタスクをキャンセル状態にしているようです。CancellationTokenSource.Cancel が呼ばれると内部でそのままコールバックが呼ばれるので本当に即キャンセルしています。(その様子はスタックトレースで見て取れます)

さて、UniTaskで動く全ての非同期処理のキャンセルが遅れていたかと言うとそうではありません。
先ほど述べたように PlayerLoop でキャンセルの確認をしている IUniTaskSource が対象なのであって、それらを使っていない非同期処理や UniTaskCompletionSource はその限りではありません。
対応されたメソッドは、こちらのPR(https://github.com/Cysharp/UniTask/pull/517) から一覧を見ることができます。

メリット

cancelImmediatelyをtrueにすると プログラムの見た目と挙動に乖離が生まれる ことを解消できます

即時にキャンセルすると何が良いのか、1つ例を出したいと思います

以下は、「カウントダウンしている間、文字が赤くなるカウントダウンタイマー」です

[SerializeField] private Button button;
[SerializeField] private Text timeText;

async UniTask StartCountdown(float seconds, CancellationToken cancellationToken)
{
    // カウントダウンの開始時に文字を赤くする
    timeText.color = Color.red;

    try
    {
        while(!cancellationToken.IsCancellationRequested)
        {
            // 時間を更新
            timeText.text = seconds.ToString("N1");
            seconds = Mathf.Max(0, seconds - Time.deltaTime);
            if(seconds <= 0) break;
            
            // cancelImmediately falseで待つ
            await UniTask.NextFrame(cancellationToken);
        }
    } finally
    {
        // カウントダウンが終わったら文字を白くする
        if(timeText) timeText.color = Color.white;
    }
}

これを任意のタイミングでキャンセルし再び起動できるようにします。

[SerializeField] private Button button;
private CancellationTokenSource timerCancellation;

private void Start()
{
    button.onClick.AddListener(() =>
    {
        timerCancellation?.Cancel();
        timerCancellation = new();
        StartCountdown(10, timerCancellation.Token).Forget();
    });
}

タイマーの起動中にボタンを押したときの挙動を UniTask.NextFrame に渡す cancelImmediately の違いでどう変わるかを見ていきます

cancelImmediately = false

e.gif

2回目の StartCountdown で文字が白くなった状態でカウントダウンしています。

e.png

cancelImmediately = true

f.gif

// cancelImmediately trueで待つ
await UniTask.NextFrame(cancellationToken, cancelImmediately: true);

無事タイマーが起動しているときだけ赤くなってくれました
文字通り即キャンセルしてくれるので以下の図のような挙動になります

f.png

こんな感じで 同じ時間に1つしか起動してほしくない、いつでもリスタートできる何かしらの非同期処理 とかに便利だったりします。UIアニメーションとかでよくある

デメリット

Note: Setting cancelImmediately to true and detecting an immediate cancellation is more costly than the default behavior. This is because it uses CancellationToken.Register; it is heavier than checking CancellationToken on the player loop.

公式のREADMEにある通り cancelImmediately がtrueだと少し重いらしいです。ほとんどの場合で気にしなくて良いとは思いますが...

まとめ

  • UniTask2.5.0 でファクトリメソッドや拡張メソッドに cancelImmediately オプションが追加された
  • 今までは UniTask.Delay などはキャンセル命令が来てから実際にキャンセル状態になるまでにラグがあった
    • UniTaskで動く全ての非同期処理にラグがあるわけではない
    • PlayerLoopでキャンセル確認しているものが対象
  • cancelImmediately をtrue にするとキャンセル命令が来たときタスクが即キャンセル状態になる
    • 内部でCancellationToken.Register使っているのでちょっと重い

おわりに

閲覧いただきありがとうございました。

今回は UniTask の新しい機能 cancelImmediately について解説しました。サンプルはこちら
間違っている箇所があれば、やさ~~~~~~~~しく教えていただけると助かります。
何卒何卒。では:raised_hand:

明日の記事は はなちるさんの『【Unity】Debug.Logはもう古い!? Unity公式のLoggingパッケージ「Unity Logging」の使い方まとめ』です!

9
7
0

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
9
7