2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity】非同期処理で同じオブジェクトの更新を連続して行う場合に前の処理をキャンセルしないと事故ることがある

Last updated at Posted at 2025-12-04

この記事はPONOS Advent Calendar 2025 5日目の記事です。


例として「同じオブジェクトの更新処理」として「同じImageに対してTextureを読み込んで設定する」というものを考えます。
Unity + UniTaskでの例ですが他の環境でも通じる話だと思います。

前準備

非同期処理を使用してSpriteを読み込むみImageに設定できる以下のようなクラスを用意しました。

using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class Sample : MonoBehaviour
{
    // UIで表示するImage
    [SerializeField]
    Image image = null;

    // Spriteを読み込んでImageに設定する
    public async UniTask UpdateTextureAsync(string textureId)
    {
        var sprite = await ResourceManager.LoadSpriteAsync(textureId);
        image.sprite = sprite;
    }
}

Sampleをアタッチしたオブジェクトを用意します。
最初は白いSpriteを表示しています。
Image Sequence_1.jpg

そこで以下の処理を呼ぶとします。

await sampleObject.UpdateTextureAsync("RedSprite"); // 赤いSpriteを設定する

すると以下のように表示されるImageは白から赤に変わります。
Image Sequence_1.jpg (読み込み前)
Image Sequence_001_0000.jpg (読み込み後)

問題にならないケース

次に以下のように連続して更新する場合について考えます。

await sampleObject.UpdateSpriteAsync("RedSprite");  // 赤いSpriteを設定する
await sampleObject.UpdateSpriteAsync("BlueSprite"); // 青いSpriteを設定する

Image Sequence_1.jpg (読み込み前)
Image Sequence_001_0000.jpg (赤読み込み後)
Image Sequence_004_0000.jpg (青読み込み後)
途中で赤になりますがきちんと最後に呼び出した青いSpriteが設定されています。
これはSpriteの更新処理を都度待機しているため問題になりません。

問題になる可能性があるケース

以下のように連続して更新処理を呼びます。ただし待機せずForgetします。

sampleObject.UpdateSpriteAsync("RedSprite").Forget();  // 赤いSpriteを設定する(待機しない)
sampleObject.UpdateSpriteAsync("BlueSprite").Forget(); // 青いSpriteを設定する(待機しない)

すると呼び出しの順番に関係なくImageに設定されるSpriteは赤になったり青になったりします。
(以下のどっちになるかわからない)
Image Sequence_001_0000.jpg Image Sequence_004_0000.jpg

問題が発生する理由

Sampleの中では以下の処理でSpriteの設定をしております。

    // Spriteを読み込んでImageに設定する
    public async UniTask UpdateTextureAsync(string textureId)
    {
        var sprite = await ResourceManager.LoadSpriteAsync(textureId);
        image.sprite = sprite;
    }

これはテクスチャが読み込め次第image.spriteに設定するという処理ですが、赤いSpriteの読み込みよりも青いSpriteの読み込みが先に完了した場合には赤いSpriteが設定されてしまいます。

実際このように連続して更新処理をForget()するようなことはまず無いと思いますが「イベントが発生したときに更新する」「イベントが連続して発生する」ようなことが組み合わさることで不具合の原因となることは良くあります。

問題の回避方法

更新処理の途中で追加の更新処理が呼ばれた場合は以前の更新をキャンセルするような実装にします。
Sampleを以下のように書き換えます。

using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

public class Sample : MonoBehaviour
{
    // UIで表示するImage
    [SerializeField]
    Image image = null;

    // UpdateSpriteAsyncをキャンセルするためのCancellationTokenSource
    CancellationTokenSource updateSpriteCancellationTokenSource = null;

    // Spriteを読み込んでImageに設定する
    public async UniTask UpdateSpriteAsync(string textureId)
    {
        // 以前の読み込みをキャンセルする
        updateSpriteCancellationTokenSource?.Cancel();

        updateSpriteCancellationTokenSource = new();
        var ct = updateSpriteCancellationTokenSource.Token;

        var sprite = await ResourceManager.LoadSpriteAsync(textureId);
        if (ct.IsCancellationRequested)
        {
            // LoadSpriteAsyncの完了までにキャンセルされていたら
            // 何もせずにreturnする
            return;
        }
        image.sprite = sprite;
    }
}

重要なのはCancellationTokenSource updateSpriteCancellationTokenSourceです。
UpdateSpriteAsyncの中ではupdateSpriteCancellationTokenSource.Cancel()を呼ぶことで以前の処理をキャンセルし、キャンセルされていればimage.spriteには値を設定しないようにします。
こうすることによって必ず最後に呼ばれた処理のSpriteが設定されるようになります。

// 以下の処理は赤いSpriteによる更新がキャンセルされ必ず青いSpriteが設定される
sampleObject.UpdateSpriteAsync("RedSprite").Forget();
sampleObject.UpdateSpriteAsync("BlueSprite").Forget();

ちなみにupdateSpriteCancellationTokenSource.Tokenはきちんとローカル変数として保持しておかないとupdateSpriteCancellationTokenSourceが新規に作成されたときにTokenも新しくなってしまうため別途保持しておきましょう。

外部からのキャンセルやオブジェクト破棄のキャンセルにも対応する

呼び出し元からのキャンセルされた場合はオブジェクト自体の破棄で処理をキャンセルしたい場合もあるかと思います。
その場合はCancellationTokenを引数にとりCancellationTokenSource.CreateLinkedTokenSourceを使用することで対応できます。

public async UniTask UpdateSpriteAsync(string textureId, CancellationToken cancellationToken)
{
    // 以前の読み込みをキャンセルする
    updateSpriteCancellationTokenSource?.Cancel();

    // Cancel()を呼ぶか外部からキャンセルされるかオブジェクトが破棄されたときにキャンセルされる
    // CancellationTokenSourceを生成する
    updateSpriteCancellationTokenSource =
        CancellationTokenSource.CreateLinkedTokenSource(
            cancellationToken,
            destroyCancellationToken);
    var ct = updateSpriteCancellationTokenSource.Token;

    // 省略
}

まとめ

  • 非同期処理で同じオブジェクトを更新するときは前の更新処理をキャンセルする
  • キャンセルするためにはCancellationTokenSourceを生成する
  • 複数のキャンセル条件にも対応するにはCancellationTokenSource.CreateLinkedTokenSourceを使用する

次回は @ackyla さんです。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?