Primeiro, vamos falar sobre corrotinas. Eles foram introduzidos no Unity em 2011, mesmo antes de async / await aparecer no .NET. No Unity, as corrotinas nos permitem executar um conjunto de instruções em vários quadros, em vez de executá-las todas de uma vez. Eles são semelhantes aos threads, mas são leves e integrados ao loop de atualização do Unity, tornando-os adequados para o desenvolvimento de jogos.
Para criar uma co-rotina, você precisa declarar uma função com o tipo de retorno IEnumerator
. Essa função pode conter qualquer lógica que você deseja que a co-rotina execute.
Para iniciar uma co-rotina, você precisa chamar o método StartCoroutine
em uma instância MonoBehaviour
e passar a função co-rotina como um argumento:
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"); } }
Existem várias instruções de rendimento disponíveis no Unity, como WaitForSeconds
, WaitForEndOfFrame
, WaitForFixedUpdate
, WaitForSecondsRealtime
, WaitUntil
e algumas outras. É importante lembrar que seu uso leva a alocações, portanto, devem ser reutilizados sempre que possível.
Por exemplo, considere este método da documentação:
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); } }
A cada iteração do loop, uma nova instância de new WaitForSeconds(.1f)
será criada. Em vez disso, podemos mover a criação para fora do loop e evitar alocações:
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**; } }
Outra propriedade importante a ser observada é que yield return
pode ser usado com todos os métodos Async
fornecidos pelo Unity porque AsyncOperation
s são descendentes de YieldInstruction
:
yield return SceneManager.LoadSceneAsync("path/to/scene.unity");
MonoBehaviour
que a inicia. Se o GameObject
for desligado ou destruído, a co-rotina para de ser processada.try-catch-finally
não pode ser usada devido à presença da sintaxe yield.yield return
antes que o próximo código comece a ser executado.As promessas são um padrão para organizar e tornar as operações assíncronas mais legíveis. Eles se tornaram populares devido ao seu uso em muitas bibliotecas JavaScript de terceiros e, desde o ES6, foram implementados nativamente.
A principal maneira de interagir com uma promessa é por meio de funções de retorno de chamada .
De acordo com essas organização Promises/A+, uma Promise pode estar em um dos três estados:
Pending
: o estado inicial, isso significa que a operação assíncrona ainda está em andamento e o resultado da operação ainda não é conhecido.Fulfilled
( Resolved
): o estado resolvido é acompanhado por um valor que representa o resultado da operação.Rejected
: se a operação assíncrona falhar por qualquer motivo, a promessa é considerada "rejeitada". O estado rejeitado é acompanhado pelo motivo da falha.
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; }
Também poderíamos envolver corrotinas em uma 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(); }
E, claro, você pode organizar qualquer combinação de ordem de execução de promessa usando ThenAll
/ Promise.All
e 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") ) );
Na história do .NET, podemos distinguir as seguintes etapas:
BeginSmth
retorna a interface IAsyncResult
. O método EndSmth
usa IAsyncResult
; se a operação não for concluída no momento da chamada EndSmth
, o thread será bloqueado.Task
e Task<TResult>
.
Para criar um método assíncrono, o método deve ser marcado com a palavra-chave async
, conter um await
dentro e o valor de retorno deve ser Task
, Task<T>
ou void
(não recomendado).
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 }
Neste exemplo, a execução ocorrerá assim:
SyncMethodA
) será executado.await Task.Delay(1000)
é iniciada e espera-se que seja executada. Enquanto isso, o código a ser chamado quando a operação assíncrona for concluída (a "continuação") será salvo.SyncMethodB
) começará a ser executada.await Task.Delay(2000)
) é iniciada e espera-se que seja executada. Ao mesmo tempo, a continuação — o código após a segunda operação assíncrona ( SyncMethodC
) será preservada.SyncMethodC
será executado, seguido pela execução e aguardando a terceira operação assíncrona await Task.Delay(3000)
.
Você também pode organizar qualquer combinação de ordens de execução usando WhenAll
e 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"); });
O compilador C# transforma chamadas async/await em uma máquina de estado IAsyncStateMachine
, que é um conjunto sequencial de ações que devem ser executadas para concluir a operação assíncrona.
Assim, o método Example
é transformado em criar e inicializar uma máquina de estado com a anotação [AsyncStateMachine(typeof(ExampleStateMachine))]
, e a própria máquina de estado possui um número de estados igual ao número de chamadas await.
Exemplo do método transformado 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; }
Exemplo de uma máquina de estado gerada 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) { /*...*/ } }
Na chamada AwaitUnsafeOnCompleted
, o contexto de sincronização atual SynchronizationContext
será obtido. SynchronizationContext é um conceito em C# usado para representar um contexto que controla a execução de um conjunto de operações assíncronas. Ele é usado para coordenar a execução do código em vários threads e para garantir que o código seja executado em uma ordem específica. O principal objetivo do SynchronizationContext é fornecer uma maneira de controlar o agendamento e a execução de operações assíncronas em um ambiente multithread.
Em diferentes ambientes, o SynchronizationContext
tem diferentes implementações. Por exemplo, em .NET, existem:
System.Windows.Threading.DispatcherSynchronizationContext
System.Windows.Forms.WindowsFormsSynchronizationContext
System.Threading.WinRTSynchronizationContext
System.Web.AspNetSynchronizationContext
O Unity também tem seu próprio contexto de sincronização, UnitySynchronizationContext
, que nos permite usar operações assíncronas com ligação à API PlayerLoop. O exemplo de código a seguir mostra como girar um objeto em cada quadro usando 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; } } }
Graças ao UnitySynchronizationContext
, podemos usar métodos UnityEngine
com segurança (como Debug.Log()
) logo após a conclusão de uma operação assíncrona, pois a execução desse código continuará no thread principal do Unity.
Esta classe permite que você gerencie um objeto Task
. Ele foi criado para adaptar antigos métodos assíncronos ao TAP, mas também é muito útil quando queremos envolver uma Task
em torno de alguma operação de longa duração que ocorre em algum evento.
No exemplo a seguir, o objeto Task
dentro de taskCompletionSource
será concluído após 3 segundos do início e obteremos seu resultado no método 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); } }
Um token de cancelamento é usado em C# para sinalizar que uma tarefa ou operação deve ser cancelada. O token é passado para a tarefa ou operação e o código dentro da tarefa ou operação pode verificar o token periodicamente para determinar se a tarefa ou operação deve ser interrompida. Isso permite um cancelamento limpo e elegante de uma tarefa ou operação, em vez de simplesmente eliminá-la abruptamente.
O padrão geral se assemelha ao uso de TaskCompletionSource
. Primeiro, um CancellationTokenSource
é criado, então seu Token
é passado para a operação assíncrona:
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(); } }
Quando a operação for cancelada, uma OperationCanceledException
será lançada e a propriedade Task.IsCanceled
será definida como true
.
É importante observar que os objetos Task
são gerenciados pelo tempo de execução .NET, não pelo Unity, e se o objeto que executa a tarefa for destruído (ou se o jogo sair do modo de jogo no editor), a tarefa continuará sendo executada como o Unity não há como cancelá-lo.
Você sempre precisa acompanhar await Task
com o CancellationToken
correspondente. Isso leva a alguma redundância de código e, no Unity 2022.2, os tokens integrados no nível MonoBehaviour
e todo o nível Application
apareceram.
Vejamos como o exemplo anterior muda ao usar o destroyCancellationToken
do objeto MonoBehaviour
:
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); } } }
Não precisamos mais criar manualmente um CancellationTokenSource
e concluir a tarefa no método OnDestroy
. Para tarefas não associadas a um MonoBehaviour
específico, podemos usar UnityEngine.Application.exitCancellationToken
. Isso encerrará a tarefa ao sair do Modo Play (no Editor) ou ao sair do aplicativo.
Task
são muito complicados e causam muitas alocações.Task
não corresponde ao encadeamento do Unity (encadeamento único).
A biblioteca ignora essas restrições sem usar threads ou SynchronizationContext
. Ele atinge a ausência de alocações usando o tipo baseado em estrutura UniTask<T>
.
Além disso, você pode converter todas as AsyncOperations
em UnitTask
com métodos de extensão:
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 } } }
Neste exemplo, o método LoadAsset
usa Resources.LoadAsync
para carregar um ativo de forma assíncrona. O método AsUniTask
é usado para converter o AsyncOperation
retornado por LoadAsync
em um UniTask
, que pode ser aguardado.
Como antes, você pode organizar qualquer combinação de ordem de execução usando UniTask.WhenAll
e 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 } }
No UniTask, há outra implementação do SynchronizationContext
chamada UniTaskSynchronizationContext
que pode ser usada para substituir UnitySynchronizationContext
para melhor desempenho.
Na primeira versão alfa do Unity 2023.1, a classe Awaitable
foi introduzida. Coroutines aguardáveis são tipos semelhantes a tarefas compatíveis com async/await projetados para serem executados no Unity. Ao contrário das Tarefas .NET, elas são gerenciadas pelo mecanismo, não pelo tempo de execução.
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"); // ... }
Eles podem ser aguardados e usados como o tipo de retorno de um método assíncrono. Em comparação com System.Threading.Tasks
, eles são menos sofisticados, mas usam atalhos de aprimoramento de desempenho com base em suposições específicas do Unity.
Awaitable
só pode ser aguardado uma vez; não pode ser aguardado por várias funções assíncronas.Awaiter.GetResults()
não bloqueará até a conclusão. Chamá-lo antes que a operação seja concluída é um comportamento indefinido.ExecutionContext
. Por motivos de segurança, as Tarefas .NET capturam contextos de execução durante a espera para propagar contextos de representação em chamadas assíncronas.SynchronizationContext
. As continuações de corrotina são executadas de forma síncrona a partir do código que gera a conclusão. Na maioria dos casos, isso será do quadro principal do Unity.ObjectPool
foi aprimorado para evitar verificações de limites Stack<T>
em sequências get/release típicas geradas por máquinas de estado assíncronas.
Para obter o resultado de uma operação demorada, você pode usar o tipo Awaitable<T>
. Você pode gerenciar a conclusão de um Awaitable
usando AwaitableCompletionSource
e AwaitableCompletionSource<T>
, semelhante a 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); } }
Às vezes é necessário realizar cálculos massivos que podem levar ao congelamento do jogo. Para isso, é melhor usar os métodos Awaitable: BackgroundThreadAsync()
e MainThreadAsync()
. Eles permitem que você saia do thread principal e retorne a ele.
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); }
Unidade | Corrotinas | Promessas | Tarefas .NET | UniTask | Tokens de cancelamento integrados | API aguardável |
---|---|---|---|---|---|---|
5.6 | ✅ | ✅ | | | | |
2017.1 | ✅ | ✅ | ✅ | | | |
2018.4 | ✅ | ✅ | ✅ | ✅ | | |
2022.2 | ✅ | ✅ | ✅ | ✅ | ✅ | |
2023.1 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |