Cysharp/UniTask

WaitWhileでcancelImmediatelyをtrueにすると、NullReferenceExceptionを発生させる

teck124 opened this issue · 3 comments

Unity: 2022.3.47f1
UniTask Version: 2.5.5

新しく作成したプロジェクトに以下のComponent WaitWhileTest をAttachして実行すると、以下のエラーが発生します。
なお、cancelImmediatelyをfalseにすると発生しません。

NullReferenceException: Object reference not set to an instance of an object
Cysharp.Threading.Tasks.UniTask+WaitWhilePromise.MoveNext () (at ./Library/PackageCache/com.cysharp.unitask@f7b3c2fbe1/Runtime/UniTask.WaitUntil.cs:254)
--- End of stack trace from previous location where exception was thrown ---
Cysharp.Threading.Tasks.UniTask+WaitWhilePromise.GetResult (System.Int16 token) (at ./Library/PackageCache/com.cysharp.unitask@f7b3c2fbe1/Runtime/UniTask.WaitUntil.cs:218)
TaskObj.Test (System.Threading.CancellationToken token) (at Assets/Scripts/WaitWhileTest.cs:69)
Cysharp.Threading.Tasks.UniTask+ExceptionResultSource.GetResult (System.Int16 token) (at ./Library/PackageCache/com.cysharp.unitask@f7b3c2fbe1/Runtime/UniTask.Factory.cs:238)
Cysharp.Threading.Tasks.UniTaskExtensions.Forget (Cysharp.Threading.Tasks.UniTask task) (at ./Library/PackageCache/com.cysharp.unitask@f7b3c2fbe1/Runtime/UniTaskExtensions.cs:557)
UnityEngine.Debug:LogException(Exception)
Cysharp.Threading.Tasks.UniTaskScheduler:PublishUnobservedTaskException(Exception) (at ./Library/PackageCache/com.cysharp.unitask@f7b3c2fbe1/Runtime/UniTaskScheduler.cs:90)
Cysharp.Threading.Tasks.UniTaskExtensions:Forget(UniTask) (at ./Library/PackageCache/com.cysharp.unitask@f7b3c2fbe1/Runtime/UniTaskExtensions.cs:561)
WaitWhileTest:Update() (at Assets/Scripts/WaitWhileTest.cs:45)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class WaitWhileTest : MonoBehaviour
{
    private const float c_CallInterval = 0.3f;
    private const float c_CancelInterval = 0.5f;
    private float m_JustBeforeCancelTime;
    private float m_JustBeforeCallTime;

    private TaskObj m_TestObj;
    private CancellationTokenSource m_TokenSource;

    private void ResetTokenSource()
    {
        m_TokenSource?.Cancel();
        m_TokenSource?.Dispose();
        m_TokenSource = new CancellationTokenSource();
        m_TestObj = new TaskObj();
    }
    // Start is called before the first frame update
    void Start()
    {
        m_JustBeforeCallTime = m_JustBeforeCancelTime = Time.unscaledTime;
        ResetTokenSource();
    }

    // Update is called once per frame
    void Update()
    {
        if (Time.unscaledTime - m_JustBeforeCancelTime > c_CancelInterval)
        {
            m_JustBeforeCancelTime = Time.unscaledTime;
            ResetTokenSource();
        }

        if (Time.unscaledTime - m_JustBeforeCallTime > c_CallInterval)
        {
            m_JustBeforeCallTime = Time.unscaledTime; 
            m_TestObj.Test(m_TokenSource.Token).Forget();
        }
    }

    void OnDestory()
    {
        m_TokenSource?.Cancel();
        m_TokenSource?.Dispose();
    }
}


public class TaskObj
{
    private CancellationTokenSource m_CancelTokenSource;
    private const float c_FinishElapsedTime = 0.1f; 
    private float m_StartTime;
    public async UniTask Test(CancellationToken token)
    {
        try
        {
            CancelAndDisposeTokenSource();
            m_CancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
            m_StartTime = Time.unscaledTime;
            await UniTask.WaitWhile(IsContinued, cancellationToken:m_CancelTokenSource.Token, cancelImmediately: true);
            Debug.Log("Task Finished");
        }
        catch (OperationCanceledException)
        {
            Debug.LogWarning("Task Canceled");
        }
        finally
        {
            // CancelAndDisposeTokenSource();
        }
    }

    private void CancelAndDisposeTokenSource()
    {
        m_CancelTokenSource?.Cancel();
        m_CancelTokenSource?.Dispose();
        m_CancelTokenSource = null;
    }

    private bool IsContinued()
    {
        return Time.unscaledTime - m_StartTime > c_FinishElapsedTime;
    }
}

余分な処理があったので、削除しました。以下のコードでも再現します。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Cysharp.Threading.Tasks;
using UnityEngine;

public class WaitWhileTest : MonoBehaviour
{
    private const float c_CallInterval = 0.3f;
    private float m_JustBeforeCallTime;

    private TaskObj m_TestObj;

    // Start is called before the first frame update
    void Start()
    {
        m_JustBeforeCallTime = Time.unscaledTime;
        m_TestObj = new TaskObj();
    }

    // Update is called once per frame
    void Update()
    {
        if (Time.unscaledTime - m_JustBeforeCallTime > c_CallInterval)
        {
            m_JustBeforeCallTime = Time.unscaledTime; 
            m_TestObj.Test(CancellationToken.None).Forget();
        }
    }
}


public class TaskObj
{
    private CancellationTokenSource m_CancelTokenSource;
    private const float c_FinishElapsedTime = 0.1f; 
    private float m_StartTime;
    public async UniTask Test(CancellationToken token)
    {
        try
        {
            CancelAndDisposeTokenSource();
            m_CancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
            m_StartTime = Time.unscaledTime;
            await UniTask.WaitWhile(IsContinued, cancellationToken:m_CancelTokenSource.Token, cancelImmediately: true);
            Debug.Log("Task Finished");
        }
        catch (OperationCanceledException)
        {
            Debug.LogWarning("Task Canceled");
        }
        finally
        {
            CancelAndDisposeTokenSource();
        }
    }

    private void CancelAndDisposeTokenSource()
    {
        m_CancelTokenSource?.Cancel();
        m_CancelTokenSource?.Dispose();
        m_CancelTokenSource = null;
    }

    private bool IsContinued()
    {
        return Time.unscaledTime - m_StartTime > c_FinishElapsedTime;
    }
}

ありがとうございます!
バグ確認できました&原因特定しました!
今日中に修正版をリリースしますのでお待ちくださいー。

fixed at 2.5.6.