首先,让我们谈谈协程。它们于 2011 年在 Unity 中引入,甚至在 .NET 中出现 async / await 之前。在 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) { ca = 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) { ca = alpha; renderer.material.color = c; yield return **waitForSeconds**; } }
另一个需要注意的重要属性是yield return
可以与 Unity 提供的所有Async
方法一起使用,因为AsyncOperation
是YieldInstruction
的后代:
yield return SceneManager.LoadSceneAsync("path/to/scene.unity");
MonoBehaviour
严格相关。如果GameObject
被关闭或销毁,协程将停止处理。try-catch-finally
结构。yield return
之后,在下一个代码开始执行之前,至少会经过一帧。Promises是一种用于组织异步操作并使异步操作更具可读性的模式。由于它们在许多第三方 JavaScript 库中的使用而变得流行,并且自 ES6 以来,它们已被原生实现。
与 Promise 交互的主要方式是通过回调函数。
根据 Promises/A+ 组织这些,Promise 可以处于以下三种状态之一:
Pending
:初始状态,这意味着异步操作仍在进行中,操作的结果尚不可知。Fulfilled
( Resolved
):已解决的状态伴随着一个代表操作结果的值。Rejected
:如果异步操作因任何原因失败,则称 Promise 被“拒绝”。拒绝状态伴随着失败的原因。
var promise = MakeRequest("//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(); }
当然,您可以使用ThenAll
/ Promise.All
和ThenRace
/ Promise.Race
组织承诺执行顺序的任意组合:
// 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") ) );
在 .NET 的发展历程中,可以分为以下几个阶段:
BeginSmth
方法返回IAsyncResult
接口。 EndSmth
方法采用IAsyncResult
;如果在调用EndSmth
时操作尚未完成,则线程将被阻塞。Task
和Task<TResult>
改进了这个概念。
要创建一个异步方法,该方法必须用关键字async
标记,内部包含一个await
,返回值必须是Task
、 Task<T>
或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)
) 已启动并预计将被执行。同时,continuation — 第二个异步操作 ( SyncMethodC
) 之后的代码将被保留。SyncMethodC
,接着执行等待第三次异步操作await Task.Delay(3000)
。
您还可以使用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"); });
C#编译器将 async/await 调用转换为IAsyncStateMachine
状态机,这是完成异步操作必须执行的一组顺序操作。
因此, Example
方法被转换为使用注解[AsyncStateMachine(typeof(ExampleStateMachine))]
创建和初始化状态机,并且状态机本身具有与 await 调用次数相等的状态数。
转换方法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) { /*...*/ } }
在AwaitUnsafeOnCompleted
调用中,将获取当前同步上下文SynchronizationContext
。 SynchronizationContext是C#中的一个概念,用来表示控制一组异步操作执行的上下文。它用于协调跨多个线程的代码执行,并确保代码按特定顺序执行。 SynchronizationContext 的主要目的是提供一种在多线程环境中控制异步操作的调度和执行的方法。
在不同的环境中, SynchronizationContext
有不同的实现。例如,在 .NET 中,有:
System.Windows.Threading.DispatcherSynchronizationContext
System.Windows.Forms.WindowsFormsSynchronizationContext
System.Threading.WinRTSynchronizationContext
System.Web.AspNetSynchronizationContext
Unity也有自己的同步上下文UnitySynchronizationContext
,它使我们能够通过绑定到 PlayerLoop API 使用异步操作。以下代码示例显示了如何使用Task.Yield()
在每个帧中旋转对象:
private async void Start() { while (true) { transform.Rotate(0, Time.deltaTime * 50, 0); await Task.Yield(); } }
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("//api.example.com/data"); return response; } } }
感谢UnitySynchronizationContext
,我们可以在异步操作完成后立即安全地使用UnityEngine
方法(例如Debug.Log()
),因为此代码的执行将在主 Unity 线程中继续。
此类允许您管理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); } }
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
。
重要的是要注意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
。这将在退出播放模式(在编辑器中)或退出应用程序时终止任务。
Task
对象过于繁琐,造成多次分配。Task
与 Unity 线程(单线程)不匹配。
库在不使用线程或SynchronizationContext
情况下绕过了这些限制。它通过使用基于UniTask<T>
结构的类型实现了没有分配。
您还可以使用扩展方法将所有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
以获得更好的性能。
在 Unity 2023.1 的第一个 alpha 版本中,引入了Awaitable
类。 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 特定假设的性能增强捷径。
Awaitable
对象只能等待一次;它不能被多个异步函数等待。Awaiter.GetResults()
在完成之前不会阻塞。在操作完成之前调用它是未定义的行为。ExecutionContext
。出于安全原因,.NET 任务在等待时捕获执行上下文,以便跨异步调用传播模拟上下文。SynchronizationContext
。协程延续从引发完成的代码同步执行。在大多数情况下,这将来自 Unity 主框架。ObjectPool
已得到改进,以避免在异步状态机生成的典型获取/释放序列中进行Stack<T>
边界检查。
要获得长时间操作的结果,您可以使用Awaitable<T>
类型。您可以使用AwaitableCompletionSource
和AwaitableCompletionSource<T>
管理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); }
实行 | 协程 | 承若书 | .NET 成就 | 单每日任务 | 内嵌撤销令牌 | 等候API |
---|---|---|---|---|---|---|
5.6 | ✅ | ✅ | | | | |
2017.1 | ✅ | ✅ | ✅ | | | |
2018.4 | ✅ | ✅ | ✅ | ✅ | | |
2022.2 | ✅ | ✅ | ✅ | ✅ | ✅ | |
2023.1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |