Введение в асинхронное программирование с Awaitable
Awaitable класс — это собственный тип Unity, который можно ожидать и использовать как возвращаемый тип async в модели асинхронного программирования C#. Большинство асинхронных API Unity поддерживают async и await модели, включая:
- Сопроцедуры Unity:
NextFrameAsync,WaitForSecondsAsync,EndOfFrameAsync,FixedUpdateAsync - Переключение на фоновый поток или главный поток
- Все типы, унаследованные от
AsyncOperation - Unity Мероприятия
- Асинхронный GPU Запись
Вы можете использовать Awaitable класс и с await оператора и в качестве async return тип в вашем собственном коде, следующим образом:
async Awaitable<List<Achievement>> GetAchievementsAsync()
{
var apiResult = await SomeMethodReturningATask(); // or any await-compatible type
List<Achievement> achievements = JsonConvert.DeserializeObject<List<Achievement>>(apiResult);
return achievements;
}
async Awaitable ShowAchievementsView()
{
ShowLoadingOverlay();
List<Achievement> achievements = await GetAchievementsAsync();
HideLoadingOverlay();
ShowAchivementsList(achievements);
}
Ожидаемое по сравнению с задачей .NET
Awaitable разработан для того, чтобы предложить более эффективную альтернативу .NET Task для асинхронного кода в проектах Unity. Эффективность Awaitable имеет некоторые важные ограничения по сравнению с Task.
Наиболее существенным ограничением является то, что экземпляры Awaitable объединяются для ограничения выделений. Рассмотрим следующий пример:
class SomeMonoBehaviorWithAwaitable : MonoBehavior
{
public async void Start()
{
while(true)
{
// do some work on each frame
await Awaitable.NextFrameAsync();
}
}
}
Без объединения каждый экземпляр MonoBehavior в этом примере выделил бы Awaitable объект каждый кадр, увеличивая рабочая нагрузка сборщика мусора Для смягчения этого эффекта Unity возвращает Awaitable объект во внутренний Awaitable бассейн, как только он ожидал.
Важно: Пул экземпляров Awaitable означает, что никогда нельзя безопасно выполнять await более одного раза на экземпляре Awaitable. Это может привести к неопределенному поведению, например, к исключению или тупиковой ситуации.
Ожидаемое по сравнению с .NET ValueTask
.NET ValueTask<TResult> предлагает некоторые из тех же ключевых преимуществ и ограничений, что и Awaitable. Типичное рекомендуемое использование ValueTask — это для асинхронных рабочих нагрузок, которые, как ожидается, будут выполняться синхронно большую часть времени. Дополнительные сведения см. в Понимание почему, что и когда ValueTask.
Ожидаемое, Задача и сводка ValueTask
Следующая таблица резюмирует сравнение функций между Unity Awaitable класс и .NET Task и ValueTask:
| Особенности | Task |
ValueTask |
UnityEngine.Awaitable |
|---|---|---|---|
| Требуемые ассигнования | Многие. Выделяет при каждом вызове методу Task-returning, увеличивая использование памяти и нагрузку сборщика мусора. |
По мере необходимости. Может быть оптимизировано с помощью пулирования. |
Минимальный по мере необходимости. Вызов метода Awaitable-returning обычно не выделяет памяти, поскольку экземпляры Awaitable по умолчанию объединяются в пул. |
| Безопасный для ожидания несколько раз | Да. | Нет. Необходимо преобразовать в Task с ValueTask.AsTask. |
Нет. Необходимо преобразовать в Task с помощью пользовательских методов расширения AsTask, см. Ожидание несколько раз в одном и том же методе в примерах кода. |
| Продолжения работают асинхронно | Да. Использование контекста синхронизации по умолчанию, в противном случае использование ThreadPool. Это увеличивает задержку при завершении на главном потоке в Unity, потому что код должен ждать следующего кадра Update для возобновления. |
Да. Оптимизирован для случаев, когда ожидаемые задачи завершаются синхронно. Если они завершаются асинхронно, поведение продолжения эквивалентно Task. |
Нет. Продолжение выполняется синхронно при инициировании завершения, то есть код немедленно возобновляется в том же кадре, в котором инициировано завершение. Дополнительную информацию см. в Ожидаемое завершение и продолжение. |
| Завершение можно запустить кодом | Да. Использование TaskCompletionSource. |
Неприменимо в типичном сценарии использования, т.е. для задач, выполняемых в основном синхронно. | Да. Использование AwaitableCompletionSource. |
| может возвращать значение | Да. Использование Task<TResult>. |
Да. Использование ValueTask<TResult>. |
Да. Использование UnityEngine.Awaitable<T>. |
Встроенная поддержка WaitAll и WaitAny |
Да. | Нет. Необходимо преобразовать в Task с помощью ValueTask.AsTask. |
Нет. Необходимо преобразовать в Task с помощью настраиваемых методов расширения AsTask, см. Ожидаемая обвязка в задаче .NET в справочнике примеров кода. |
| Unity планирование выполнения потоков и циклов обновления | Нет. | Нет. | Да. Вы можете указать, на какой поток возобновляется Awaitable с помощью Awaitable.BackgroundThreadAsync и Awaitable.MainThreadAsync. Вы также можете запланировать работу относительно циклов Update или FixedUpdate с помощью Awaitable.NextFrameAsync и Awaitable.FixedUpdateAsync. Для более подробной информации см. Ожидаемое завершение и продолжение. |
Когда использовать Awaitable над Задачей или ValueTask
Выбор API зависит от профиля производительности вашего асинхронного кода, но в целом:
Taskявляется единственным вариантом, когда нужно ждать несколько раз или от нескольких потребителей одновременно.ValueTaskявляется хорошим выбором, если у вас есть высокопроходный асинхронный код, который завершается синхронно большую часть времени.-
Awaitableявляется хорошим выбором, когда:- Вам не нужно ждать свои методы несколько раз и ожидать, что они в основном завершатся асинхронно.
- Вы хотите, чтобы ваши асинхронные задачи имели встроенную поддержку специфических для Unity концепций, таких как основной поток и циклы Обновление и FixedUpdate.
Ожидаемое по сравнению с iterator-based coroutines
Сопроцедуры Awaitable обычно более эффективны, чем сопроцедуры, основанные на итераторах, особенно в случаях, когда итератор возвращает ненулевые значения, такие как WaitForFixedUpdate.
Однако преимущество в производительности сопутствующих процедур Awaitable уменьшается, когда вы запускаете много из них одновременно. Например, MonoBehaviour, такой как в предыдущем примере кода, который ожидает Awaitable.NextFrameAsync в цикле while, может вызвать проблемы производительности, если он будет прикреплен к каждой GameObject в большом проекте.
Примечание: Вы можете безопасно yield return год Awaitable от традиционного iterator-основанного coroutine, но вы не можете yield return год Awaitable<T0>.