はじめに
こちらの記事は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
にキャンセル例外を吐かせ、前後のフレームを見るというシンプルなものです
結果は、catch句に入った時点では1フレーム遅れていることがわかります
UniTask.Delay
は毎フレーム タスクが完了・キャンセルされているかを確認しています
その確認の後にキャンセルされていたら、同フレームであっても例外が吐かれるのはその次のフレームの確認の際になります
余談ですが、確認のタイミングは引数の 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();
}
キャンセルした同フレームでcatch句に入っていることがわかります
cancelImmediately
オプションを true にすると、即座に例外が吐かれるようになります
該当の実装を見ると、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
2回目の StartCountdown で文字が白くなった状態でカウントダウンしています。
cancelImmediately = true
// cancelImmediately trueで待つ
await UniTask.NextFrame(cancellationToken, cancelImmediately: true);
無事タイマーが起動しているときだけ赤くなってくれました
文字通り即キャンセルしてくれるので以下の図のような挙動になります
こんな感じで 同じ時間に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
について解説しました。サンプルはこちら
間違っている箇所があれば、やさ~~~~~~~~しく教えていただけると助かります。
何卒何卒。では
明日の記事は はなちるさんの『【Unity】Debug.Logはもう古い!? Unity公式のLoggingパッケージ「Unity Logging」の使い方まとめ』です!