Комментарии 25
Самая большая проблема async/await это остановка корутины. Использование CancellationToken громоздко и неудобно. Этот токен нужно протаскивать во все корутины, и во все вложенные корутины тоже. А еще его нужно генерировать. Вы в примерах показываете только передачу токена в корутину. А ведь если посмотреть внешнюю обвязку, то там кода для CancellationToken еще на добрый экран наберется.
Токен генерируется через CancellationTokenSource. А оно в свою очередь должно быть объявлено полем в MonoBehaviour. И еще оно IDisposable, а значит его нужно корректно диспозить. А что бы его диспозить нужно вызвать его Dispose в событии OnDestroy. А вот это событие вызывается не всегда (и при краше приложения и в редакторе). В результате чего асинхронная операция продолжает себе спокойно выполняться дальше после того как основной поток уже умер или вышел из плей-мода.
В общем, CancellationToken это боль. А вот с классической корутиной таких проблем нет. Вызвал StopCoroutine и все.
К тому же, зависимость корутины от MonoBehaviour вы почему-то записываете в минусы. Но на самом деле, привязка жизни корутины к жизни объекта сцены вполне логична и удобна.
Если же нужно сделать непривязанную ни к чему корутину, то это делается легко через какой нибудь самописный Dispatcher который у всех давно уже есть.
В целом async/await это хорошо и правильно. Но вот проблема остановки таких корутин к сожалению останавливает от их использования.
>Использование CancellationToken громоздко и неудобно.<
Такой подход является стандартным для .NET мира. С таким же успехом можно критиковать в целом подход к работе с async\await в .NET и тасками.
> И еще оно IDisposable, а значит его нужно корректно диспозить <
При работе с файлами точно также нужно вручную освобождать ресурсы или вы и от этого инструмента отказались?
>нужно вызвать его Dispose в событии OnDestroy. А вот это событие вызывается не всегда<
Особенность работы жизненного цикла монобехов: "OnDestroy will only be called on game objects that have previously been active."
> В результате чего асинхронная операция продолжает себе спокойно выполняться дальше после того как основной поток уже умер или вышел из плей-мода.<
"Runs completely on Unity's PlayerLoop so doesn't use threads and runs on WebGL, wasm, etc."
>К тому же, зависимость корутины от MonoBehaviour вы почему-то записываете в минусы. Но на самом деле, привязка жизни корутины к жизни объекта сцены вполне логична и удобна.<
Корутины могут не иметь ничего общего с объектами в сцене.
>Если же нужно сделать непривязанную ни к чему корутину, то это делается легко через какой нибудь самописный Dispatcher который у всех давно уже есть. <
Мне кажется не стоит говорить за всех. По моим наблюдениям в районе половины компаний используют UniRx\UniTask вместо того, что бы изобретать свои велосипеды.
К слову, а всякие WhenAll\Any \ContinueWith и тп вы тоже будете велосипедить? =)
Такой подход является стандартным для .NET мира. С таким же успехом можно критиковать в целом подход к работе с async\await в .NET и тасками.
Для .Net мира может и является стандартным, а для Unity мира - нет. В стандартном .Net не было корутин, вместо них появилось решение async/await. Но в юнити такое решение как корутина было всегда. И оно более удобно в этом плане.
А вообще, я вам про то что это не удобно, а вы мне "это стандартное решение".
При работе с файлами точно также нужно вручную освобождать ресурсы или вы и от этого инструмента отказались?
При работе с файлами я использую using. Это удобно. А вот с CancellationTokenSource так не получится.
К тому же, чтение файлов, так скажем, не самая частая операция в игровом движке.
По моим наблюдениям в районе половины компаний используют UniRx\UniTask вместо того, что бы изобретать свои велосипеды. К слову, а всякие WhenAll\Any \ContinueWith и тп вы тоже будете велосипедить?
Ну хорошо, что используют.
Еще раз: стандартную корутину можно запустить без привязки к объекту. А так-то, да, я люблю велосипеды :)
>Для .Net мира может и является стандартным, а для Unity мира - нет <
Ну во первых, Unity3d часть мира .NET, а во вторых, UniTask это реализация Task для Unity3d. По этому было бы странно, если бы реализация была иной.
>Но в юнити такое решение как корутина было всегда. И оно более удобно в этом плане.<
Не знаю, где вы там удобство обнаружили, когда с помощью этой штуки даже значение вернуть нельзя, не говоря уже о хоть чуточку сложных кейсах, где нужно оперировать группой корутин.
>Ну во первых, Unity3d часть мира .NET, а во вторых, UniTask это реализация Task для Unity3d. По этому было бы странно, если бы реализация была иной.
Это не является контраргументом к тому, что корутина более удобна в вышеуказанном контексте, заявление про всякие миры - не релевантно.
> когда с помощью этой штуки даже значение вернуть нельзя
Как это нельзя, это же IEnumerator, yield return никто не отменял.
>Как это нельзя, это же IEnumerator, yield return никто не отменял.<
Ну покажите пример, тогда поговорим, на счет удобства=)
Coroutine<Texture> cor = StartCoroutine<Texture>(GetAvatarCor(url));
yield return cor.coroutine;
try {
texture = cor.Value;
} catch (Exception e) {
throw new Exception(e.Message);
}
Не похоже на валидный код, по крайней мере для 2020. В исходниках монбеха нет возможности запустить корутину с дженериком.
Ну, это не коробочное решение, это чей то велосипед. Выше же заявлялось, что: "корутина более удобна". Возможно я ошибаюсь, но мне кажется, что если бы это было так, то не было бы необходимости изобретать вокруг этой технологии какие то велосипеды, что бы вернуть значение, диспатчеры и тп. А если требуются такие велосипеды, то наверное все же лучше использовать проверенные решения типа UniRx\UniTask?
Не больший велосипед, чем "некоробочные" Uni*. Код как код, работает (с 2012 года). Речь шла о том, может ли корутина возвращать значения, поддерживает ли обработку исключений. Да и да. Что лучше использовать, зависит от задачи. Нет универсальных под все подходящих инструментов.
>Не больший велосипед, чем "некоробочные" Uni*. Код как код, работает (с 2012 года). <
Предложенный вами велоспед, это велосипед во круг решения в котором проблемы с аллокациями и перформенсом.
"Некоробочные" же предлагают лучший перформенс, с куда меньшим количеством аллокаций и богатым API.
Мне кажется, что только супермозг откажется от бесплатного перформенса из за того, что нужно менеджить лайфтайм.
>Речь шла о том, может ли корутина возвращать значения, поддерживает ли обработку исключений. Да и да.<
А почему опускается, тот факт, что нужно сначала запилить велоспед?=)
Вообще, можно сделать свой объект для ожидания с CTS на Update и/или с проверкой на запуск и состояние объекта. Это был бы очень полезный функционал, жаль, что Unity по факту не поддерживает всё это из коробки. Как было в далёкой 1(3) версии, так всё и осталось. Но плюсы для меня перевешивают даже эти минусы, а CTS на деле не так уж и часто нужен, почти всегда можно обойтись без него.
Насчёт OnDestroy, не помню такого, чтобы не было вызова, если объект действительно уничтожался. При краше, возможно, но в таком случае уже и не важно.
Основной поток и при ошибке, и при установке не умирает. Он продолжает работать и не в плеймоде.
Основной плюс не в замене корутин. Это вообще мелочь, которая нужна для раскрытия всего потенциала. С async/await и Task можно легко и просто делать сложные вещи, которые без этого требуют кучу кода и много расчётов, при этом ещё и писать легко читаемый код. Управление загрузкой(вообще, не только файлов), управление стейтами, понятное и простое разделение осуществляемых действий(задач) на Task-и. А так же легкая и непринуждённая их комбинация с минимальными изменениями в коде. Даже то, что промисами делалось не даёт такой простой работы и содержит кучу скобочек, вложенных методов, да и всякого левого кода. Код стал вновь как на лабе в универе, легко читаем и джуном. Правда только в области логики, потому как сопровождать и оборачивать юнити приблуды нужно. Но весь бойлерплейт отделён от логики. Если бы Unity поддерживали таски по умолчанию во всех компонентах, то вообще сказочно вышло бы)
Ну все-таки совсем от юнитёвских корутин отказываться не оправдано. Привязка к монобехам даёт интуитивную и удобную инфраструктуру для сборки вьюх на корутинах (очень быстро можно сделать на ней state machine для управления состояниями представления), вместо использования Update. А вот внутрянку лучше действительно делать на async/await во всех остальных местах, где нет монобехов. Сеть однозначно туда отходит, вообще работа с общими ограниченными ресурсами типа файловых систем и пр. Что-то сродни подхода node.js.
Я пришёл к тому, что стоит использовать Animator и не пилить велосипеды. Вариативность высокая, анимации настроить можно и художникам отдать и не трогать программистов по мелочам. С Task подружил и теперь могу использовать это в игре для ожидания. Хотя, было и не просто из-за того, что аниматор не даёт использовать имена стейтов. Впрочем, никогда не использовал для этого корутин и не вижу в этом +. Видел то же на дотвине, довольно не плохо и всё через код - быстро. Минус только в том, что для изменений всегда нужен прогер.
Мы всей командой перешли на UniTask'и и очень довольны.
Пример демонстрирует возможность обернуть асинхронный метод в блок try/catch. А как обработать ошибку уже зависит от разработчика. И вы правильно заметили, что кому-то достаточно просто в лог писать, а кому-то необходимо сообщение на экране показать.
Поэтому конкретная реализация не приведена, а в начале статьи я предупреждаю, что весь код приведен только в качестве примера.
Одна из проблем UniTask
при запуске в редакторе Unity в том, что после остановки игры асинхронная задача продолжает выполняться, что для тех кто привык работать с корутинами окажется неожиданностью. Пришлось немного подправить код библиотеки, чтобы остановка происходила сама без явного CancellationToken
.
Звучит как что-то, что работает неправильно
Немного неверно описал, UniTask отрабатывает еще 2 цикла после выхода редактора из Play-mode, что влечет разные ошибки при обращении к разрушенным объектам. А вот дефолтный Task продолжает работать бесконечно.
Тестовый код:
public class Test : MonoBehaviour
{
private void Start()
{
RunUniTask().Forget();
RunTask();
}
private async UniTaskVoid RunUniTask()
{
while (true)
{
await UniTask.Yield();
Debug.Log($"UniTask is playing: {Application.isPlaying}");
}
}
private async void RunTask()
{
while (true)
{
await Task.Yield();
Debug.Log($"Task is playing: {Application.isPlaying}");
}
}
}
Лог (UniTask успевает сработать 2 раза после остановки игры, Task продолжает работать все время):
![](https://habrastorage.org/getpro/habr/upload_files/716/ec4/a38/716ec4a3889d31fd9340a8fc3f55f65e.png)
Спасибо автору за статью ?
Подскажите, пожалуйста: правильно ли я понимаю, что и для Unity async/await эффективен, когда мы имеем дело с относительно медленными I/O операциями: прием-передача данных по сети, чтение-запись данных на HDD, тогда как для CPU-/GPU-/RAM-/VRAM-bound операций эффективней будет использовать coroutines, так как потери вычислительного времени на асинхронное переключение контекста могут лишь замедлить такие операции в сравнении с выполнением вычислений в coroutines из расчета "одна корутина на ядро"?
Эффективен он в принципе, всегда. Не за счёт производительности, а за счёт понятных и простых конструкций в коде и их вариативности использования. Корутины - это даже не прошлый век, а позапрошлый или вообще от мамонтов. До них ещё были промисы(как в дотвине) и эвенты(как замена колбекам). И всё это решало одну и ту же проблему ожидания процессов и их компоновки, и в целом использования. Но для понимания этого стоит ознакомится с ними в принципе, даже вне Unity.
Корутины могут быть быстрее, но если очень нужна производительность, то стоит работать с самим подходом - получать пул объектов, обрабатывать в циклах и т.п. Для одиночных сложных операций оверхед не существенен даже на старых мобилках и ТВ приставках. Для чего-то на уровне железа всегда циклы и массивы, ну или для того же с удобством сейчас это ECS и всё такое(таких проектов и не знаю, где это прям абсолютно нужно). Корутины в таком случае могут быть медленее в десятки раз, а значит и нет смысла с ними работать в указанных условиях.
А, ну и сам Update у объекта не стоит забывать. Часто вместо него используют корутины, что так же не правильно и приводит к проблемам. Везде можно использовать либо Task, либо Update.
Давно работаю в Unity с Task и async/await.
Пункты 1-6 работают и при стандартном Task. Пункт 7 спорный. При стандартном Task лог начинается от места старта или ожидания(SyncContext), если последнее произошло. И это отлично, если с UniTask работает лог даже при ожидании - крайне полезно получать лог как при синхронном выполнении. Только по примеру не ясно, так ли это?
Однако, не всё так хорошо и 8 я назову скорее отрицательным и ставящим крест на UniTask для повсеместного использования.
Поначалу я думал, что прошлый опыт использования оказался негативным случайно. Но читая про оптимизацию, понял, что нет. Как часто и бывает, получив некоторое ускорение быстродействия порезали ошибки и НЕЯВНО привели к ситуации с багами. Задобали тупые картинки с супералгоритмами по типу - вырежу половину функционала и получу буст! (и забывают хотя бы мелким шрифтом прописать условия при которых он будет, но которых может и на пару страниц текстом набраться)
The following operations should never be performed on a ValueTask instance:
* Awaiting the instance multiple times.
* Calling AsTask multiple times.
* Using .Result or .GetAwaiter().GetResult() when the operation hasn’t yet completed, or using them multiple times.
* Using more than one of these techniques to consume the instance.If you do any of the above, the results are undefined.
Всего то сделал из мультитула заточку. За то вес уменьшился! Да по факту же вырезана основная прелесть async/await!
Суть не в последовательных действиях, с этим и промисы справлялись не плохо, а в применении ожидания одного действия в разных местах. Когда делаешь загрузку ресурсов для игры - плевать на выделение даже мегабайта памяти - её и так выделяется много. А вот сделать менеджер загрузки ресурсов с ожиданием цепочек действий, их агрегацией, да ещё асинхронно по готовности, да буквально за пол часа сделать всё это - очень круто. Сделать загрузку плагинов с зависимостями просто поставив в методы Load ожидание окончание загрузки требуемых плагинов и запустив весь десяток-другой методов загрузки, не заботясь вообще об очерёдности. К примеру IAP, плагины издателя, Ads требуют UnityServ; а плагин издателя ещё и IAP, и FB; а FB нужны ключи сервера и IAP; ну и т.д.).
Про аллокацию так же замечу, что на структуру тоже выделяется память, тоже в неё пишется. Это ни разу не волшебная пуля и минусы в этом тоже есть. Здесь всё аналогично пулу.
Unity: 8 причин отказаться от Coroutine в пользу Async