Unity 的完整异步编程入门

Unity 的完整异步编程入门

在本文中,我们考虑 Unity 中异步编程的主要方法:讨论优缺点、说明代码示例,并展示每种方法的使用位置的实际示例。对于新手来说这是一本很好的入门书,对于更有经验的工程师来说则是一个可靠的资源!
在这里插入图片描述

游戏开发中的一些任务不是同步的——它们是异步的。这意味着它们在游戏代码中不是线性执行的。其中一些异步任务可能需要相当长的时间才能完成,而另一些则与密集计算相关。

一些最常见的游戏异步任务如下:

执行网络请求
加载场景Loading scenes、资源resources和其他资产assets
读取和写入文件
人工智能用于决策
长动画序列
处理大量数据
Pathfinding 寻找路径

现在,至关重要的是,由于所有 Unity 代码都在一个线程中运行,任何类似于上述任务之一的任务,如果同步执行,都会导致主线程被阻塞,从而导致帧丢失。
在本文中,我们将讨论如何避免此类问题。我们将推荐异步编程技术在单独的线程中执行这些任务,从而使主线程可以自由地执行其他任务。这将有助于确保游戏玩法流畅且反应灵敏,并(希望)让玩家满意。
Coroutines 协程
首先,我们来谈谈协程。它们于 2011 年在 Unity 中引入,甚至早于 async/await 在 .NET 中出现。在 Unity 中,协程允许我们在多个帧上执行一组指令,而不是一次性执行所有指令。它们与线程类似,但重量轻并集成到 Unity 的更新循环中,因此非常适合游戏开发。
(顺便说一句,从历史上看,协程是 Unity 中执行异步操作的第一种方式,因此互联网上的大多数文章都是关于它们的。)
要创建协程,您需要声明一个返回类型为 IEnumerator 的函数。该函数可以包含您希望协程执行的任何逻辑。
要启动协程,您需要在 MonoBehaviour 实例上调用 StartCoroutine 方法并将协程函数作为参数传递:

public class Example : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(MyCoroutine());
    }

    IEnumerator MyCoroutine()
    {
        Debug.Log("Starting coroutine");
      yield return null;
      Debug.Log("Executing coroutine");
      yield return null;
      Debug.Log("Finishing coroutine");
    }
}

Unity中有多种yield指令,例如 WaitForSeconds 、 WaitForEndOfFrame 、 WaitForFixedUpdate 、 WaitForSecondsRealtime 、 WaitUntil 以及其他一些人。重要的是要记住,使用它们会导致分配,因此应尽可能重用它们。
例如,考虑文档中的此方法:

IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return new WaitForSeconds(.1f);
    }
}

循环的每次迭代都会创建一个新的 new WaitForSeconds(.1f) 实例。相反,我们可以将创建移到循环之外并避免分配:

IEnumerator Fade()
{
    Color c = renderer.material.color;

  var waitForSeconds = new WaitForSeconds(0.2f);

    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return waitForSeconds;
    }
}

另一个需要注意的重要属性是 yield return 可以与 Unity 提供的所有 Async 方法一起使用,因为 AsyncOperation 是 YieldInstruction 的后代:

yield return SceneManager.LoadSceneAsync("path/to/scene.unity");

协程的一些可能的陷阱

综上所述,协程也有一些需要注意的缺点:

不可能返回长时间操作的结果。您仍然需要回调函数,这些回调函数将传递给协程并在协程完成时调用以从中提取任何数据。

协程与启动它的 MonoBehaviour 严格相关。如果 GameObject 被关闭或销毁,协程将停止处理。
由于存在yield 语法,因此无法使用 try-catch-finally 结构。

在下一个代码开始执行之前, yield return 之后至少会经过一帧。

lambda 和协程本身的分配
Promises 承诺
Promise 是一种组织异步操作并使异步操作更具可读性的模式。它们由于在许多第三方 JavaScript 库中使用而变得流行,并且自 ES6 以来已在本机实现。

使用 Promises 时,我们立即从异步函数返回一个对象。这允许调用者等待操作的解决(或错误)。

从本质上讲,这使得异步方法可以返回值并像同步方法一样“行动”:它们不是立即返回最终值,而是给出“承诺”,即它们将在将来的某个时候返回一个值。

Unity 有多种 Promise 实现:

C-Sharp-Promise
UnityFx.Async
C# Promises
uPromise

与 Promise 交互的主要方式是通过回调函数。

您可以定义一个回调函数,当 Promise 被解析时将被调用,另一个回调函数将在 Promise 被拒绝时被调用。这些回调接收异步操作的结果作为参数,然后可用于执行进一步的操作。

根据 Promises/A+ 组织的这些规范,Promise 可以处于以下三种状态之一:

Pending :初始状态,这意味着异步操作仍在进行中,操作的结果尚不清楚。

Fulfilled ( Resolved ):已解决的状态附带一个表示操作结果的值。

Rejected :如果异步操作由于任何原因失败,则称 Promise 被“拒绝”。被拒绝的状态附有失败的原因。
More on Promises 更多关于承诺的内容

此外,Promise 可以链接在一起,以便一个 Promise 的结果可以用来确定另一个 Promise 的结果。

For example, you can create a Promise that fetches some data from a server, and then use that data to create another Promise that performs some calculation and other actions:
例如,您可以创建一个从服务器获取一些数据的 Promise,然后使用该数据创建另一个执行某些计算和其他操作的 Promise:

var promise = MakeRequest(“https://some.api”)
.Then(response => Parse(response))
.Then(result => OnRequestSuccess(result))
.Then(() => PlaySomeAnimation())
.Catch(exception => OnRequestFailed(exception));

下面是如何组织执行异步操作的方法的示例:

public IPromise<string> MakeRequest(string url)
{
    // Create a new promise object
    var promise = new Promise<string>();
    // Create a new web client
    using var client = new WebClient();
    
    // Add a handler for the DownloadStringCompleted event
    client.DownloadStringCompleted += (sender, eventArgs) =>
    {
        // If an error occurred, reject the promise
        if (eventArgs.Error != null)
        {
            promise.Reject(eventArgs.Error);
        }
        // Otherwise, resolve the promise with the result
        else
        {
            promise.Resolve(eventArgs.Result);
        }
    };

    // Start the download asynchronously
    client.DownloadStringAsync(new Uri(url), null);

    // Return the promise
    return promise;
}

我们还可以将协程包装在 Promise 中:

void Start()
{
    // Load the scene and then show the intro animation
    LoadScene("path/to/scene.unity")
        .Then(() => ShowIntroAnimation())
        .Then( ... );
}

// Load a scene and return a promise
Promise LoadScene(string sceneName)
{
    // Create a new promise
    var promise = new Promise();
    // Start a coroutine to load the scene
    StartCoroutine(LoadSceneRoutine(promise, sceneName));
    // Return the promise
    return promise;
}

IEnumerator LoadSceneRoutine(Promise promise, string sceneName)
{
    // Load the scene asynchronously
    yield return SceneManager.LoadSceneAsync(sceneName);
    // Resolve the promise once the scene is loaded
    promise.Resolve();
}
And of course, you can organize any combination of promise execution order using ThenAll / Promise.All and ThenRace / Promise.Race:
当然,您可以使用 ThenAll / Promise.All 和 ThenRace / Promise.Race 组织 Promise 执行顺序的任意组合:

// Execute the following two promises in sequence
Promise.Sequence(
    () => Promise.All( // Execute the two promises in parallel
        RunAnimation("Foo"),
        PlaySound("Bar")
    ),
    () => Promise.Race( // Execute the two promises in a race
        RunAnimation("One"),
        PlaySound("Two")
    )
);

承诺中“没有承诺”的部分

尽管使用起来很方便,但 Promise 也有一些缺点:

开销:与使用其他异步编程方法(例如协程)相比,创建 Promise 涉及额外的开销。在某些情况下,这可能会导致性能下降。

调试:调试 Promise 可能比调试其他异步编程模式更困难。跟踪执行流程并识别错误来源可能很困难。

异常处理:与其他异步编程模式相比,Promise 的异常处理可能更复杂。管理 Promise 链中发生的错误和异常可能很困难。
Async/Await Tasks 异步/等待任务

自版本 5.0 (2012) 以来,async/await 功能已成为 C# 的一部分,并在 Unity 2017 中随 .NET 4.x 运行时的实现引入。

在.NET的历史中,可以分为以下几个阶段:

EAP(基于事件的异步模式):此方法基于操作完成时触发的事件以及调用此操作的常规方法。

APM(异步编程模型):这种方法基于两种方法。 BeginSmth 方法返回 IAsyncResult 接口。 EndSmth 方法采用 IAsyncResult ;如果 EndSmth 调用时操作尚未完成,则线程被阻塞。

TAP(基于任务的异步模式):通过引入 async/await 以及类型 Task 和 Task 改进了这个概念。

由于最后一种方法的成功,以前的方法已经过时了。

要创建异步方法,该方法必须使用关键字 async 进行标记,内部包含 await ,并且返回值必须是 Task 、 Task 或 void (不推荐)。

public async Task Example()
{
    SyncMethodA();
    await Task.Delay(1000); // the first async operation
		SyncMethodB();
    await Task.Delay(2000); // the second async operation
		SyncMethodC();
    await Task.Delay(3000); // the third async operation
}

在此示例中,执行将如下所示:

首先,将执行调用第一个异步操作 ( SyncMethodA ) 之前的代码。

第一个异步操作 await Task.Delay(1000) 已启动并预计将被执行。同时,异步操作完成(“继续”)时调用的代码将被保存。

第一个异步操作完成后,“继续”——直到下一个异步操作( SyncMethodB )的代码将开始执行。

第二个异步操作 ( await Task.Delay(2000) ) 已启动并预计将被执行。同时,延续部分 - 第二个异步操作 ( SyncMethodC ) 之后的代码将被保留。

第二个异步操作完成后,会执行 SyncMethodC ,然后执行并等待第三个异步操作 await Task.Delay(3000) 。

这是一个简化的解释,因为事实上 async/await 是语法糖,允许方便地调用异步方法并等待其完成。

您还可以使用 WhenAll 和 WhenAny 组织执行顺序的任意组合:

var allTasks = Task.WhenAll(
    Task.Run(() => { /* ... */ }), 
    Task.Run(() => { /* ... */ }), 
    Task.Run(() => { /* ... */ })
);
allTasks.ContinueWith(t =>
{
    Console.WriteLine("All the tasks are completed");
});


var anyTask = Task.WhenAny(
    Task.Run(() => { /* ... */ }), 
    Task.Run(() => { /* ... */ }), 
    Task.Run(() => { /* ... */ })
);
anyTask.ContinueWith(t =>
{
    Console.WriteLine("One of tasks is completed");
});

IAsyncStateMachine IAsync状态机

C# 编译器将 async/await 调用转换为 IAsyncStateMachine 状态机,这是完成异步操作必须执行的一组连续操作。

每次调用await操作时,状态机都会完成其工作并等待该操作完成,然后继续执行下一个操作。这使得异步操作可以在后台执行,而不会阻塞主线程,同时也使得异步方法调用更简单、更具可读性。

这样, Example 方法就转化为创建并初始化一个带注解 [AsyncStateMachine(typeof(ExampleStateMachine))] 的状态机,并且状态机本身的状态数量等于 wait 调用的数量。

转换后的方法示例 Example

[AsyncStateMachine(typeof(ExampleStateMachine))]
public /*async*/ Task Example()
{
    // Create a new instance of the ExampleStateMachine class
    ExampleStateMachine stateMachine = new ExampleStateMachine();
    
    // Create a new AsyncTaskMethodBuilder and assign it to the taskMethodBuilder property of the stateMachine instance
    stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create();
    
    // Set the currentState property of the stateMachine instance to -1
    stateMachine.currentState = -1;
    
    // Start the stateMachine instance
    stateMachine.taskMethodBuilder.Start(ref stateMachine);
    
    // Return the Task property of the taskMethodBuilder
    return stateMachine.taskMethodBuilder.Task;
}

生成的状态机示例 ExampleStateMachine

[CompilerGenerated]
private sealed class ExampleStateMachine : IAsyncStateMachine
{
    public int currentState; 
    public AsyncTaskMethodBuilder taskMethodBuilder;
    private TaskAwaiter taskAwaiter;

    public int paramInt;
    private int localInt;

    void IAsyncStateMachine.MoveNext()
    {
        int num = currentState;
        try
        {
            TaskAwaiter awaiter3;
            TaskAwaiter awaiter2;
            TaskAwaiter awaiter;
            
            switch (num)
            {
                default:
                    localInt = paramInt;  
                    // Call the first synchronous method
                    SyncMethodA();  
                    // Create a task awaiter for a delay of 1000 milliseconds
                    awaiter3 = Task.Delay(1000).GetAwaiter();  
                    // If the task is not completed, set the current state to 0 and store the awaiter
                    if (!awaiter3.IsCompleted) 
                    {
                        currentState = 0; 
                        taskAwaiter = awaiter3; 
                        // Store the current state machine
                        ExampleStateMachine stateMachine = this; 
                        // Await the task and pass the state machine
                        taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine); 
                        return;
                    }
                    // If the task is completed, jump to the label after the first await
                    goto Il_AfterFirstAwait; 
                case 0: 
                    // Retrieve the awaiter from the taskAwaiter field
                    awaiter3 = taskAwaiter; 
                    // Reset the taskAwaiter field
                    taskAwaiter = default(TaskAwaiter); 
                    currentState = -1; 
                    // Jump to the label after the first await
                    goto Il_AfterFirstAwait; 
                case 1: 
                    // Retrieve the awaiter from the taskAwaiter field
                    awaiter2 = taskAwaiter;
                    // Reset the taskAwaiter field
                    taskAwaiter = default(TaskAwaiter);
                    currentState = -1;
                    // Jump to the label after the second await
                    goto Il_AfterSecondAwait;
                case 2: 
                    // Retrieve the awaiter from the taskAwaiter field
                    awaiter = taskAwaiter;
                    // Reset the taskAwaiter field
                    taskAwaiter = default(TaskAwaiter);
                    currentState = -1;
                    break;
                    
                Il_AfterFirstAwait: 
                    awaiter3.GetResult(); 
                    // Call the second synchronous method
                    SyncMethodB(); 
                    // Create a task awaiter for a delay of 2000 milliseconds
                    awaiter2 = Task.Delay(2000).GetAwaiter(); 
                    // If the task is not completed, set the current state to 1 and store the awaiter
                    if (!awaiter2.IsCompleted) 
                    {
                        currentState = 1;
                        taskAwaiter = awaiter2;
                        // Store the current state machine
                        ExampleStateMachine stateMachine = this;
                        // Await the task and pass the state machine
                        taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
                        return;
                    }
                    // If the task is completed, jump to the label after the second await
                    goto Il_AfterSecondAwait;

                Il_AfterSecondAwait:
                    // Get the result of the second awaiter
                    awaiter2.GetResult();
                    // Call the SyncMethodC
                    SyncMethodC(); 
                    // Create a new awaiter with a delay of 3000 milliseconds
                    awaiter = Task.Delay(3000).GetAwaiter(); 
                    // If the awaiter is not completed, set the current state to 2 and store the awaiter
                    if (!awaiter.IsCompleted)
                    {
                        currentState = 2;
                        taskAwaiter = awaiter;
                        // Set the stateMachine to this
                        ExampleStateMachine stateMachine = this;
                        // Await the task and pass the state machine
                        taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                        return;
                    }
                    break;
            }

            // Get the result of the awaiter
            awaiter.GetResult();
        }
        catch (Exception exception)
        {
            currentState = -2;
            taskMethodBuilder.SetException(exception);
            return;
        }
        currentState = -2;
        taskMethodBuilder.SetResult(); 
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { /*...*/ }
}

SynchronizationContext 同步上下文
在 AwaitUnsafeOnCompleted 调用中,将获取当前同步上下文 SynchronizationContext 。 SynchronizationContext 是 C# 中的一个概念,用于表示控制一组异步操作执行的上下文。它用于协调多个线程之间的代码执行,并确保代码按特定顺序执行。 SynchronizationContext的主要目的是提供一种在多线程环境中控制异步操作​​的调度和执行的方法。

在不同的环境下, SynchronizationContext 有不同的实现。例如,在.NET中,有:

WPF: System.Windows.Threading.DispatcherSynchronizationContext WPF: System.Windows.Threading.DispatcherSynchronizationContext
WinForms: System.Windows.Forms.WindowsFormsSynchronizationContext WinForms: System.Windows.Forms.WindowsFormsSynchronizationContext
WinRT: System.Threading.WinRTSynchronizationContext WinRT: System.Threading.WinRTSynchronizationContext
ASP.NET: System.Web.AspNetSynchronizationContext ASP.NET: System.Web.AspNetSynchronizationContext
Unity also has its own synchronization context, UnitySynchronizationContext, which enables us to use asynchronous operations with binding to the PlayerLoop API. The following code example shows how to rotate an object in each frame using Task.Yield():
Unity 也有自己的同步上下文 UnitySynchronizationContext ,它使我们能够通过绑定到 PlayerLoop API 来使用异步操作。以下代码示例演示如何使用 Task.Yield() 在每个帧中旋转对象:

private async void Start()
{
    while (true)
    {
        transform.Rotate(0, Time.deltaTime * 50, 0);
        await Task.Yield();
    }
}

在Unity中使用async/await发出网络请求的另一个例子:

using UnityEngine;
using System.Net.Http;
using System.Threading.Tasks;

public class NetworkRequestExample : MonoBehaviour
{
    private async void Start()
    {
        string response = await GetDataFromAPI();
        Debug.Log("Response from API: " + response);
    }

    private async Task<string> GetDataFromAPI()
    {
        using (var client = new HttpClient())
        {
            var response = await client.GetStringAsync("https://api.example.com/data");
            return response;
        }
    }
}

感谢 UnitySynchronizationContext ,我们可以在异步操作完成后立即安全地使用 UnityEngine 方法(例如 Debug.Log() ),因为此代码的执行将继续在 Unity 主线程中。

TaskCompletitionSource
此类允许您管理 Task 对象。它的创建是为了使旧的异步方法适应 TAP,但当我们想要将 Task 包装在某个事件发生时的某些长时间运行的操作周围时,它也非常有用。
在以下示例中, taskCompletionSource 内的 Task 对象将在开始后 3 秒后完成,我们将在 Update 方法中获取其结果:

using System.Threading.Tasks;
using UnityEngine;

public class Example : MonoBehaviour
{
    private TaskCompletionSource<int> taskCompletionSource;

    private void Start()
    {
        // Create a new TaskCompletionSource
        taskCompletionSource = new TaskCompletionSource<int>();
        // Start a coroutine to wait 3 seconds 
    // and then set the result of the TaskCompletionSource
        StartCoroutine(WaitAndComplete());
    }

    private IEnumerator WaitAndComplete()
    {
        yield return new WaitForSeconds(3);
        // Set the result of the TaskCompletionSource
        taskCompletionSource.SetResult(10);
    }

    private async void Update()
    {
        // Await the result of the TaskCompletionSource
        int result = await taskCompletionSource.Task;
        // Log the result to the console
        Debug.Log("Result: " + result);
    }
}

Cancellation Token (取消令牌)
C# 中使用取消令牌来表示应取消任务或操作。令牌被传递给任务或操作,任务或操作内的代码可以定期检查令牌以确定是否应停止任务或操作。这允许干净、优雅地取消任务或操作,而不是突然终止它。

取消令牌通常用于用户可以取消长时间运行的任务或不再需要该任务的情况,例如用户界面中的取消按钮。

整体模式类似于 TaskCompletionSource 的使用。首先,创建一个 CancellationTokenSource ,然后将其 Token 传递给异步操作:

public class ExampleMonoBehaviour : MonoBehaviour
{
 private CancellationTokenSource _cancellationTokenSource;

 private async void Start()
 {
  // Create a new CancellationTokenSource
  _cancellationTokenSource = new CancellationTokenSource();
  // Get the token from the CancellationTokenSource
  CancellationToken token = _cancellationTokenSource.Token;

  try
  {
   // Start a new Task and pass in the token
   await Task.Run(() => DoSomething(token), token);
  }
  catch (OperationCanceledException)
  {
   Debug.Log("Task was cancelled");
  }
 }

 private void DoSomething(CancellationToken token)
 {
  for (int i = 0; i < 100; i++)
  {
   // Check if the token has been cancelled
   if (token.IsCancellationRequested)
   {
    // Return if the token has been cancelled
    return;
   }

   Debug.Log("Doing something...");
   // Sleep for 1 second
   Thread.Sleep(1000);
  }
 }

 private void OnDestroy()
 {
  // Cancel the token when the object is destroyed
  _cancellationTokenSource.Cancel();
 }
}

当操作被取消时,将抛出 OperationCanceledException ,并且 Task.IsCanceled 属性将设置为 true 。

Unity 2022.2 中的新异步功能

需要注意的是 Task 对象由 .NET 运行时管理,而不是由 Unity 管理,如果执行任务的对象被销毁(或者游戏在编辑器中退出播放模式),则任务将继续运行,因为 Unity 无法取消它。

您始终需要将 await Task 与相应的 CancellationToken 一起使用。这导致了一些代码冗余,在 Unity 2022.2 中出现了 MonoBehaviour 级别和整个 Application 级别的内置令牌。

让我们看看前面的示例在使用 MonoBehaviour 对象的 destroyCancellationToken 时有何变化:

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;

public class ExampleMonoBehaviour : MonoBehaviour
{
 private async void Start()
 {
  // Get the cancellation token from the MonoBehaviour
  CancellationToken token = this.destroyCancellationToken;

  try
  {
   // Start a new Task and pass in the token
   await Task.Run(() => DoSomething(token), token);
  }
  catch (OperationCanceledException)
  {
   Debug.Log("Task was cancelled");
  }
 }

 private void DoSomething(CancellationToken token)
 {
  for (int i = 0; i < 100; i++)
  {
   // Check if the token has been cancelled
   if (token.IsCancellationRequested)
   {
    // Return if the token has been cancelled
    return;
   }

   Debug.Log("Doing something...");
   // Sleep for 1 second
   Thread.Sleep(1000);
  }
 }
}

我们不再需要手动创建 CancellationTokenSource 并在 OnDestroy 方法中完成任务。对于与特定 MonoBehaviour 无关的任务,我们可以使用 UnityEngine.Application.exitCancellationToken 。这将在退出播放模式(在编辑器中)或退出应用程序时终止任务。

UniTask
尽管 .NET 任务使用起来很方便并提供了功能,但在 Unity 中使用时它们有明显的缺点:

Task 对象太麻烦并且会导致很多分配。

Task 与 Unity 线程(单线程)不匹配。

UniTask 库绕过了这些限制,无需使用线程或 SynchronizationContext 。它通过使用 UniTask 基于结构的类型来实现无需分配。

UniTask 需要 .NET 4.x 脚本运行时版本,Unity 2018.4.13f1 是官方支持的最低版本。

您还可以使用扩展方法将所有 AsyncOperations 转换为 UnitTask :

using UnityEngine;
using UniTask;

public class AssetLoader : MonoBehaviour
{
    public async void LoadAsset(string assetName)
    {
        var loadRequest = Resources.LoadAsync<GameObject>(assetName);
        await loadRequest.AsUniTask();

        var asset = loadRequest.asset as GameObject;
        if (asset != null)
        {
            // Do something with the loaded asset
        }
    }
}

在此示例中, LoadAsset 方法使用 Resources.LoadAsync 异步加载资源。然后使用 AsUniTask 方法将 LoadAsync 返回的 AsyncOperation 转换为可以等待的 UniTask 。

和以前一样,您可以使用 UniTask.WhenAll 和 UniTask.WhenAny 组织执行顺序的任意组合:

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

public class Example : MonoBehaviour
{
    private async void Start()
    {
        // Start two Tasks and wait for both to complete
        await UniTask.WhenAll(Task1(), Task2());

        // Start two Tasks and wait for one to complete
        await UniTask.WhenAny(Task1(), Task2());
    }

    private async UniTask Task1()
    {
        // Do something
    }

    private async UniTask Task2()
    {
        // Do something
    }
}

在 UniTask 中,还有另一个 SynchronizationContext 实现,称为 UniTaskSynchronizationContext ,可以用来替换 UnitySynchronizationContext 以获得更好的性能。

Awaitable API 等待API
在 Unity 2023.1 的第一个 alpha 版本中,引入了 Awaitable 类。可等待协程是异步/等待兼容的类似任务类型,旨在在 Unity 中运行。与 .NET 任务不同,它们由引擎而不是运行时管理。

private async Awaitable DoSomethingAsync()
{
  // awaiting built-in events
   await Awaitable.EndOfFrameAsync();
   await Awaitable.WaitForSecondsAsync();

  // awaiting .NET Tasks
  await Task.Delay(2000, destroyCancellationToken);
   await Task.Yield();

  // awaiting AsyncOperations
   await SceneManager.LoadSceneAsync("path/to/scene.unity");

   // ...
}

它们可以被等待并用作异步方法的返回类型。与 System.Threading.Tasks 相比,它们不太复杂,但根据 Unity 特定的假设采取了提高性能的捷径。

以下是与 .NET 任务相比的主要区别:

Awaitable 对象只能等待一次;它不能被多个异步函数等待。

Awaiter.GetResults() 在完成之前不会阻塞。在操作完成之前调用它是未定义的行为。

切勿捕获 ExecutionContext 。出于安全原因,.NET 任务在等待时捕获执行上下文,以便跨异步调用传播模拟上下文。

切勿捕获 SynchronizationContext 。协程延续是从引发完成的代码同步执行的。在大多数情况下,这将来自 Unity 主框架。

可等待对象是池化对象,以防止过度分配。这些是引用类型,因此可以跨不同堆栈引用它们、高效复制等等。 ObjectPool 已得到改进,以避免异步状态机生成的典型获取/释放序列中的 Stack 边界检查。

要获取长时间操作的结果,可以使用 Awaitable 类型。您可以使用 AwaitableCompletionSource 和 AwaitableCompletionSource 管理 Awaitable 的完成,类似于 TaskCompletitionSource :

using UnityEngine;
using Cysharp.Threading.Tasks;

public class ExampleBehaviour : MonoBehaviour
{
    private AwaitableCompletionSource<bool> _completionSource;

    private async void Start()
    {
        // Create a new AwaitableCompletionSource
        _completionSource = new AwaitableCompletionSource<bool>();

        // Start a coroutine to wait 3 seconds
        // and then set the result of the AwaitableCompletionSource
        StartCoroutine(WaitAndComplete());

        // Await the result of the AwaitableCompletionSource
        bool result = await _completionSource.Awaitable;
        // Log the result to the console
        Debug.Log("Result: " + result);
    }

    private IEnumerator WaitAndComplete()
    {
        yield return new WaitForSeconds(3);
        // Set the result of the AwaitableCompletionSource
        _completionSource.SetResult(true);
    }
}

有时需要执行大量计算,这可能会导致游戏冻结。为此,最好使用 Awaitable 方法: BackgroundThreadAsync() 和 MainThreadAsync() 。它们允许您退出主线程并返回主线程。

private async Awaitable DoCalculationsAsync()
{
  // Awaiting execution on a ThreadPool background thread.
  await Awaitable.BackgroundThreadAsync();

  var result = PerformSomeHeavyCalculations();

  // Awaiting execution on the Unity main thread.
  await Awaitable.MainThreadAsync();

  // Using the result in main thread
  Debug.Log(result);
}

这样,Awaitables 消除了使用 .NET 任务的缺点,并且还允许等待 PlayerLoop 事件和 AsyncOperations。
我们可以看到,随着Unity的发展,组织异步操作的工具越来越多:

我们已经考虑了 Unity 中异步编程的所有主要方式。根据任务的复杂性和所使用的 Unity 版本,您可以使用从协程和 Promise 到任务和等待的各种技术,以确保游戏中流畅、无缝的游戏体验。感谢您的阅读,我们等待您的下一个杰作。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小云同志你好

谁能书阁下,白首太玄经

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值