Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inconsistency when using UniTask to wait Addressables LoadAsync #649

Open
TamuraKazunori opened this issue Jan 24, 2025 · 0 comments
Open

Comments

@TamuraKazunori
Copy link
Contributor

TamuraKazunori commented Jan 24, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant