Как стать автором
Обновить
15
0

Пользователь

Отправить сообщение

Без примера кода сложно сказать почему. Но такое возможно, если использовать UniTask.RunOnThreadPool или UniTask.SwitchToThreadPool. Тогда задача запустится на ThreadPool, а не на PlayerLoop.

Нужно же всё в одном. Множество запросов, множество задач, которые хотят использовать эти данные, а их использование зависит от других задач. ... Концептуально другое использование.

Можете привести рабочий пример с кодом? Не совсем понимаю, какое концептуально другое использование async/await тут может быть.

Я не вижу смысла в использовании UniTask повсеместно из-за его ограничений.

Про какие ограничения речь? Приведите пример с Task и покажите, почему то же самое невозможно реализовать с UniTask.

Спасибо за Ваши комментарии. Благодаря им моя следующая статья про детальный разбор отличий UniTask от Task будет куда богаче, так как становится понятнее, в чём заключается основное недопонимание работы с async/await и с UniTask в частности.

Касательно вопросов.

А если нужно множество запросов?

Оставляете начальную реализацию NetworkService без изменений.

А если ожидать данные с одного запроса нужно из нескольких мест и мы не контролируем момент получения результата и запроса?

Меняете реализацию NetworkService на предложенный вариант с AsyncLazy.

придётся генерить классы.

Реализация через Task и реализация через AsyncLazy будет генерировать одинаковое кол-во классов.

Такое ощущение, что Вам лично не приятен UniTask, но я же не заставляю Вас его использовать. Используйте Task. Но статья от этого актуальности не теряет, ведь основной упор сделан на работу с async/await, а что для этого использовать – решать Вам.

P.S. Если про неприязнь к UniTask я не прав, вместо догадок предлагаю открыть IDE и попробовать реализовать все Ваши идеи. И если останутся вопросы, то приходите с конкретными примерами. Но перед этим перечитайте ещё раз статью внимательнее. В ней есть ответы на все Ваши вопросы.

Для запуска вне основного потока надо использовать Task.Run(). Так называемая "настоящая асинхронность" потоков будет работать только в этом случае.

Не совсем так. В другой поток мы можем попасть и так:

private async Task StartOperationAsync()
{
    await WaitAsync().ConfigureAwait(false);

    // Not a main thread.
}

private async Task WaitAsync()
{
    // Still the main thread.

    await Task.Yield();
}

Если говорим про UniTask, то можем так:

private async UniTask StartOperationAsync()
{
    await UniTask.SwitchToThreadPool();

    // Not a main thread.
    print(Thread.CurrentThread.ManagedThreadId);

    await UniTask.SwitchToMainThread();
}

As a limitation, all UniTask objects cannot await twice, as they automatically return to the pool when the await completes.

Если ознакомиться со статьёй внимательнее, а именно с разделом Concurrency, то можно там найти следующее:

Обратите внимание, что поле _longServerRequestTask у нас типа AsyncLazy<Data>, а не UniTask<Data>. Всё потому, что UniTask не позволяет выполнить await задачи более одного раза, как и ValueTask. Связано это с тем, что UniTask – это переиспользуемая структура, и после await объект возвращается в пул. Повторный вызов await выбросит исключение, так как объект уже может использоваться в другом месте.

Там же приведен пример, как одну и туже задачу мы await'им сколь угодно раз, используя AsyncLazy.

Если коротко, то UniTask был написан специально для Unity, тесно интегрирован с игровым циклом и учитывает его особенности. Т.е. например, ситуации, когда запустил задачу, а потом вышел из Play Mode, а задача продолжает работать, не будет.

По поводу стека вызовов. Не совсем понял.

Следующий код:

public class AwaitAllTheWayTest : MonoBehaviour
{
    [ContextMenu(nameof(StartOperation))]
    public void StartOperation()
    {
        FirstAsync(destroyCancellationToken).Forget();
    }

    private async UniTaskVoid FirstAsync(CancellationToken cancellationToken)
    {
        await SecondAsync(cancellationToken);
    }

    private async UniTask SecondAsync(CancellationToken cancellationToken)
    {
        await ThirdAsync(cancellationToken);
    }

    private async UniTask ThirdAsync(CancellationToken cancellationToken)
    {
        await UniTask.Delay(1000, cancellationToken: cancellationToken);
        throw new Exception();
    }
}

Выдаст такой стек вызовов:

Exception: Exception of type 'System.Exception' was thrown.

AwaitAllTheWay.AwaitAllTheWayTest.ThirdAsync (at Assets/Scripts/AwaitAllTheWay/AwaitAllTheWayTest.cs:29)
...
AwaitAllTheWay.AwaitAllTheWayTest.SecondAsync (at Assets/Scripts/AwaitAllTheWay/AwaitAllTheWayTest.cs:23)
...
AwaitAllTheWay.AwaitAllTheWayTest.FirstAsync (at Assets/Scripts/AwaitAllTheWay/AwaitAllTheWayTest.cs:18)
...
AwaitAllTheWay.<FirstAsync>d__1:MoveNext() (at Assets/Scripts/AwaitAllTheWay/AwaitAllTheWayTest.cs:18)
..
AwaitAllTheWay.<SecondAsync>d__2:MoveNext() (at Assets/Scripts/AwaitAllTheWay/AwaitAllTheWayTest.cs:23)
...
AwaitAllTheWay.<ThirdAsync>d__3:MoveNext() (at Assets/Scripts/AwaitAllTheWay/AwaitAllTheWayTest.cs:29)
...

А как добавление суффикса Async к async void методу решает описанную проблему? Это только больше вводит в заблуждение. Почему метод асинхронный, а я не могу воспользоваться ключевым словом await? Это то же самое, что взять плохой код, но при этом назвать его красиво, например, «Photon».

По поводу добавления суффикса Async к методам, которые возвращают UniTask, Task и т.д. согласен. Во всех приведённых примерах эта конвенция наименования соблюдена.

P.S. Все названия и события вымышлены. Любые совпадения с реальными продуктами случайны.

Всё верно. А ещё GetCancellationTokenOnDestroy() под капотом вызывает TryGetComponent(). Поэтому, если планируете его использовать, лучше кэшировать CancellationToken на старте.

В статье про это есть)

В C# async void методы обычно используются для сценариев «запустил и забыл». В идеале эти методы не должны быть блокирующими и, как правило, используются только в обработчиках событий или асинхронном коде верхнего уровня.

Так что же вам не мешает это сделать?

public class MyFirstDocumentView : DocumentView<MyFirstViewModel>
{
    protected override void OnInit()
    {
        base.OnInit();

        RootVisualElement
            .Q<BindableTextField>("FirstNameTextField")
            .BindingValuePath = nameof(BindingContext.FirstName);

        RootVisualElement
            .Q<BindableTextField>("LastNameTextField")
            .BindingValuePath = nameof(BindingContext.LastName);

        RootVisualElement
            .Q<BindableLabel>("FullNameLabel")
            .BindingTextPath = nameof(BindingContext.FullName);
    }
}

Минус очевиден - лишний код, в котором могут быть допущены ошибки. И вам все равно в UXML файле необходимо будет задать имена компонентов. Так проще там сразу задать свойство для связывания, чем городить такой огород.

Пример демонстрирует возможность обернуть асинхронный метод в блок try/catch. А как обработать ошибку уже зависит от разработчика. И вы правильно заметили, что кому-то достаточно просто в лог писать, а кому-то необходимо сообщение на экране показать.

Поэтому конкретная реализация не приведена, а в начале статьи я предупреждаю, что весь код приведен только в качестве примера.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность