Skip to content

Inconsistency when using UniTask to wait Addressables LoadAsync #649

Open
@TamuraKazunori

Description

@TamuraKazunori

Describe the bug

When using Addressables.LoadAsync<Object>(key).ToUniTask(ct), the asset returned is sometimes not the one specified by the key, but rather an asset whose loading was previously canceled.

To Reproduce

Steps to reproduce the behavior:

  1. Call Addressables.LoadAsync<Object>(key).ToUniTask(ct) with a particular key.
  2. Cancel the load operation.
  3. Call Addressables.LoadAsync<Object>(newKey).ToUniTask(newCt).
  4. Observe that the asset returned is not associated with the newKey but is instead related to the first key.

Expected behavior

The asset returned should be the one specified by the latest key provided.

Investigation and Possible Cause

  • It appears that the load handle remains alive, even if the UniTask waiting on the load is canceled. Addressables seem to retain a reference until the operation completes.
  • When a UniTask is canceled, the AsyncOperationHandleConfiguredSource is returned to the pool, and the same instance is reused for the next load operation.
  • When the previous load eventually completes, it invokes the handle's completion callback, which mistakenly triggers the AsyncOperationHandleConfiguredSource.Complete for an unrelated wait operation.

Proposed Solution

Insert handle.Completed -= completedCallback within the following if statement:

if (cancellationToken.IsCancellationRequested)
{
completed = true;
if (autoReleaseWhenCanceled && handle.IsValid())
{
Addressables.Release(handle);
}
core.TrySetCanceled(cancellationToken);
return false;
}

This change should ensure that the completion callback is not unintentionally invoked by unrelated load operations.

Sample Code

public class LoadTest : MonoBehaviour
{
    private CancellationTokenSource _cts;

    private void Start()
    {
        TestAsync().Forget();
    }

    private async UniTask TestAsync()
    {
        await Addressables.InitializeAsync();

        _cts = new CancellationTokenSource();
        LoadAssetAsync("test1", _cts.Token).Forget();
        await UniTask.Yield();
        _cts.Cancel();
        _cts.Dispose();
        await UniTask.Yield();
        LoadAssetAsync("test2", default).Forget();
    }

    private async UniTask LoadAssetAsync(string address, CancellationToken ct)
    {
        var handle = Addressables.LoadAssetAsync<Object>(address);
        try
        {
            var asset = await handle.ToUniTask(cancellationToken: ct);
            Debug.Log($"{address} : {asset.name}");
        }
        finally
        {
            Addressables.Release(handle);
        }
    }
}

Conditions for Reproduction

  • Addressables should be set up with assets having keys "test1" and "test2".
  • When loading "test2", ensure that the load for "test1" has not yet completed.
  • The load operation for "test1" must complete faster than that for "test2".
  • The AsyncOperationHandleConfiguredSource used for the "test2" load must be the same instance as that used for the "test1" load.

Result
In the log output for the "test2" load, the name of the asset from "test1" (which was supposed to be canceled) is displayed.

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions