Pull to refresh

Comments 8

Вот и выросло то поколение, которое искренне удивляется тому, появление чего мы наблюдали в реальном времени. Пора на пенсию? 😅

Разбирал эту механику с дизасмом, если интересно: https://habr.com/ru/articles/1047532/
Если в двух абзацах, то:
Проблема лежит даже не этажом ниже, а в самом подвале. Корнями в обработку оконных сообщений в COM. Древний механизм, который нельзя просто выкинуть, иначе все поломается.
EventHandler возвращает void не потому, что так решили в .NET. Событие в UI поднимается из WndProc, а сообщение туда попало через PostMessage. У PostMessage нет возвращаемого значения, нет способа дождаться обработки, и нет места, куда положить ошибку. Все ваши проблемы - это просто исключение некуда деть, операцию не дождаться, побочные эффекты приезжают когда попало - это ровно свойства PostMessage, увиденные из C#. То есть async void не некий компромисс с моделью событий .NET, а компромисс с обработкой оконных сообщений в COM, которая старше C# лет так на пятнадцать.
Дальше куда чудесатее, но уверен, вы справитесь сами )

Мне всё-таки кажется что это неудачное решение - разрешить async void.

При наличии методов вроде Task.Run и Task.Factory.StartNew всегда можно запустить задачу как fire-and-forget, невзирая на ограничение сигнатуры ивента:

void OnButtonClick(ButtonClickEventArgs args, object? obj)
{
  Task.Run(DoWorkAsync);
}

И в коде эта семантика fire-and-forget намного более явная, и труднее забыть про обработку исключений в таком коде.

Неудачный пример. fire-and-forget в UI в нормальных прогах - редкость. Чаще всего требуется обратная связь. Хотя бы банальное: заблочить кнопку перед вызовом фоновой задачи и разблочить после.

Но если у нас уже есть ивент, у которого сигнатура не предполагает Task (а именно - void), то выбор у нас небольшой: либо fire-and-forget, либо блокирующий вызов sync-over-async. Ну или ждать пока разработчики фреймворка "пофиксят" это, добавив поддержку асинхронности =)

Либо async void. Про блокирующие вызовы и так понятно - это не про UI. Но у Task.Run есть свои проблемы, которые как раз "решает" async void.

В async void асинхронные вызовы сохраняют контекст синхронизации, так что тебе в итоге требуется оборачивать в Task.Run только тяжёлые куски с вычислениями.
В случае Task.Run, чтобы поменять что-то в VM или UI напрямую, придётся извращаться Dispatcher.Invoke - втыкать этот вызов где попадётся.

А на практике именно так и будет: ты делаешь Task.Run, а потом начинаешь ловить "миллион" ошибок о недопустимом потоке. Начинаешь втыкать Dispatcher.Invoke и потом напарываешься на рассинхрон в UI из-за того, что два свойства в двух разных Invoke поменялись. Или того хуже: тормоза анимации.
В общем, в WPF я этого наелся в своё время, так что на желание запустить Task.Run сразу при нажатии кнопки, смотрю с некоторым отвращением =/

А с чего вдруг вообще сравниваются async Task и async void? Из-за схожего ключевого слова?
отличия от ValueTask и Task надо рассматривать не в контексте ивента, а в контексте треда, на котором идет исполнение, потому что именно MainThread - это ключевое условие любого UI фреймворка и именно для них async void является абсолютно преимущественным вызовом.

Самое простое применение в UI фреймворках:

async void OnButtonClick(object sender, EventArgs e)
{
  Loading = true; // UI Thread
  await GetDataFromApi(); // сразу кидает await Task.Yield() - НЕ UI THREAD!
  Loading = false; // UI Thread
}

В этом вызове отлично видно, что UI поток может спокойно продолжать работать дальше, пока весь асинхронный код выполняется в бэкграунде.
Кстати, у консольных приложений или при определенных настройках в UI, можно продолжить выполнять код в бэкграунде после первого async вызова.

Аналогичный результат через void (без async) можно получить с троекратными переворотами в воздухе и с необходимостью писать 2 функции и 3 вызова на каждый такой запрос:
void Func -> async Task Func -> Shell.Current.Dispatcher.DispatchAsync(() => Func)
(или любой другой аналог возврата к UI Thread)

По моему мнению, async void и async Task - это два абсолютно несравнимых зверя для абсолютно разного контекста, а ответ на вопрос интервьюера должен был быть не вот в этом посте, а в исключительно в смысловой части

В третьем пункте, если метод IncrementCounterAsync() будет возвращать Task, всё равно будет гонка. Причина же не в войде или таске, а в том что запускаются оба вызвова IncrementCounterAsync() без await.

PS А void авейтить нельзя.

PPS Метод с async void попадёт в стеймашину, но не позволит ожидать асинхронного результата.

Sign up to leave a comment.

Articles