如何避免异步无效?

Evo*_*lor 7 c# unity-game-engine async-await

注意:这个问题是 Unity 特有的。

我读到这async void是要避免的。 我尝试使用 来执行此操作Result,但我的应用程序一直锁定。如何避免使用 async void?

public async void PrintNumberWithAwait()
{
    int number = await GetNumber();
    Debug.Log(number); //Successfully prints "5"
}

public void PrintNumberWithResult()
{
    int number = GetNumber().Result;
    Debug.Log(number); //Application Freezes
}

private async Task<int> GetNumber()
{
    await Task.Delay(1000);
    return 5;
}
Run Code Online (Sandbox Code Playgroud)

我认为这是正确的,但我一定错过了一些东西。如何在没有 的情况下使用 async/await async void

我使用以下代码单独运行测试(一次注释掉一个):

PrintNumberWithAwait();
PrintNumberWithResult();
Run Code Online (Sandbox Code Playgroud)

Fed*_*rio 8

简洁版本

Unity 的同步上下文是单线程的。所以:

  1. 结果挂起直到任务完成
  2. 该任务无法完成,因为主线程上已推送继续,即正在等待

详细版本

你说你正在使用Unity。Unity“99%”是一个单线程框架。所以我想这段代码是在主 UI 线程上执行的。

让我们详细了解一下执行 PrintNumberWithResult() 时代码执行的操作。

  1. [主线程]从 PrintNumberWithResult() 调用 GetNumber()
  2. [主线程]你执行await Task.Delay()
  3. [主线程] await 行下的代码(返回 5)被“推送”到“代码列表”中,以便在任务(延迟)完成后执行。 小见解:这种“延续代码”列表是由一个名为 SynchronizationContext 的类处理的(它是 ac# 的东西,而不是 Unity 的东西)。您可以将此类视为负责说明如何以及何时调用等待之间的代码的人。标准 .NET 实现使用线程池(即一组线程),它将在任务完成后执行代码。这就像“高级回调”。现在在 Unity 中情况有所不同。他们实现了一个自定义同步上下文,确保所有代码始终在主线程中执行。我们现在可以继续
  4. [主线程]延迟任务尚未完成,因此您可以在 PrintNumberWithResult 方法中提前返回,并执行 .Result,这使得主线程挂起在那里,直到任务完成。
  5. [僵局]。此时发生了两件事。主线程正在等待任务完成。Custom Unity 的同步上下文将代码推送到等待之上,以便在主线程中执行。但主线程正在等待!所以它永远不会自由地执行该代码。

解决方案 永远不要调用.Result。

如果您想触发并忘记任务操作,请使用 Task.Run(() => )。可是等等!这在 Unity 中不是一个好主意!(也许它在其他 .NET 应用程序中)。

如果您在 Unity 中使用 Task.Run(),您将强制使用默认的 .NET TaskScheduler 执行异步代码,该任务使用线程池,并且如果您调用某些与 Unity 相关的 API,则可能会导致一些同步问题。

在这种情况下,您想要做的是使用 async void (实际上并不是因为与异常处理相关的原因),一个您永远不会等待的异步任务方法(更好),或者可能使用一个库作为UniTask来使用 Unity 进行异步等待(我认为最好的)。


der*_*ugo 5

这个答案非常完整,并且很好地解释了“问题”/主题。

但稍微扩展一下,您的async void“作品”示例有一个原因:Debug.Log同步上下文并不重要。您可以安全地Debug.Log退出不同的线程和后台任务,Unity 会在控制台中处理它们。但是,一旦您尝试使用仅在主线程上允许的 Unity API 中的任何内容(基本上是立即依赖于或更改场景内容的所有内容),它可能会中断,因为不能保证最终会出现await在主线程上。

然而,现在怎么办?

没有什么真正反对在 Unity 中使用Threadand !Task.Run

您只需确保将所有结果分派回 Unity 主线程即可。

所以我只想举一些实际的例子来说明如何做到这一点。


经常使用的东西是所谓的“主线程调度程序”..基本上只是一个ConcurrentQueue允许您从任何线程回调,然后Enqueue在Unity 主线程上的例程中调用它们的东西。ActionTryDequeueUpdate

这看起来有点像例如

/// <summary>
/// A simple dispatcher behaviour for passing any multi-threading action into the next Update call in the Unity main thread
/// </summary>
public class MainThreadDispatcher : MonoBehaviour
{
    /// <summary>
    /// The currently existing instance of this class (singleton)
    /// </summary>
    private static MainThreadDispatcher _instance;

    /// <summary>
    /// A thread-safe queue (first-in, first-out) for dispatching actions from multiple threads into the Unity main thread
    /// </summary>
    private readonly ConcurrentQueue<Action> actions = new ConcurrentQueue<Action>();

    /// <summary>
    /// Public read-only property to access the instance
    /// </summary>
    public static MainThreadDispatcher Instance => _instance;

    private void Awake ()
    {
        // Ensure singleton 
        if(_instance && _instance != this)
        {
            Destroy (gameObject);
            return;
        }

        _instance = this;

        // Keep when the scene changes 
        // sometimes you might not want that though
        DontDestroyOnLoad (gameObject);
    }

    private void Update ()
    {
        // In the main thread work through all dispatched callbacks and invoke them
        while(actions.TryDequeue(out var action))
        {
            action?.Invoke();
        }
    }

    /// <summary>
    /// Dispatch an action into the next <see cref="Update"/> call in the Unity main thread
    /// </summary>
    /// <param name="action">The action to execute in the next <see cref="Update"/> call</param>
    public void DoInNextUpdate(Action action)
    {
        // Simply append the action thread-safe so it is scheduled for the next Update call
        actions.Enqueue(action);
    }
}
Run Code Online (Sandbox Code Playgroud)

当然,您需要将其附加到场景中的对象上,然后从任何您可以使用的地方,例如

public void DoSomethingAsync()
{
    // Run SomethingWrapper async and pass in a callback what to do with the result
    Task.Run(async () => await SomethingWrapper(result => 
    { 
        // since the SomethingWrapper forwards this callback to the MainThreadDispatcher
        // this will be executed on the Unity main thread in the next Update frame
        new GameObject(result.ToString()); 
    }));
}

private async Task<int> Something()
{
    await Task.Delay(3000);

    return 42;
}

private async Task SomethingWrapper (Action<int> handleResult)
{
    // cleanly await the result
    var number = await Something ();

    // Then dispatch given callback into the main thread
    MainThreadDispatcher.Instance.DoInNextUpdate(() =>
    {
        handleResult?.Invoke(number);
    });
}
Run Code Online (Sandbox Code Playgroud)

如果您正在进行大量异步操作并希望在某个时刻将它们全部分派回主线程,那么这是有意义的。

在此输入图像描述


另一种可能性是使用Coroutines。协程基本上有点像临时Update方法(默认情况MoveNext下每帧调用IEnumerator一次),因此您可以重复检查您的任务是否已在主线程上完成。这就是 Unity 自身的用途,例如UnityWebRequest,看起来有点像

public void DoSomethingAsync()
{
    // For this this needs to be a MonoBehaviour of course
    StartCorouine (SomethingRoutine ());
}

private IEnumerator SomethingRoutine()
{
    // Start the task async but don't wait for the result here
    var task = Task.Run(Something);

    // Rather wait this way
    // Basically each frame check if the task is still runnning
    while(!task.IsCompleted)
    {
        // Use the default yield which basically "pauses" the routine here
        // allows Unity to execute the rest of the frame
        // and continues from here in the next frame
        yield return null;
    }
    // basically same as
    //yield return new WaitUntil(()=>task.IsCompleted);

    // Safety check if the task actually finished or is canceled or faulty
    if(task.IsCompletedSuccessfully)
    {
        // Since we already waited until the task is finished 
        // This time Result will not freeze the app
        var number = task.Result;
        new GameObject (number.ToString());
    }
    else
    {
        Debug.LogWarning("Task failed or canceled");
    }
}

private async Task<int> Something ()
{
    await Task.Delay(3000);

    return 42;
}
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述