Pull to refresh

Comments 27

В принципе стандартные советы. Единственный спорный момент — это Предпочитайте return Task вместо return await.


Если возвращать Task от вызова асинхронного метода без использования await, то метод, который это делает не попадёт в стектрейс, выброшенный внутри вызываемого метода. И порой это может помешать пониманию хода программы. Плюс учитывая следующий совет о неработающих try-catch и using в таких методах, то я бы сформулировал совет наоборот (у нас на работе он был в стайлгайде): всегда делайте await в середине асинхронного стека вызовов вместо return Task.

Полностью согласен, тот, кто придумал этот совет — никогда не разгребал стектрейсы после такого кода. Мы у себя тоже стараемся так не делать.

А я разгребал, и не вижу никаких сложностей.

Это замечательно, и я восхищаюсь вашим профессионализмом в этом вопросе. И, однако, сам факт того, что вы лично не увидели никаких сложностей где-то там у себя — совершенно не означает, что их ни у кого нет.
И, однако, сам факт того, что вы лично не увидели никаких сложностей где-то там у себя — совершенно не означает, что их ни у кого нет.


Так то, что вы их увидели себя — ровно также не означает, что у других они будут. Даже интересно узнать что за случай у вас там такой, по которому вы не могли понять стектрейс.

Пример:
Class1. var result = await class2.Get();
Class2. return class3.Get();
Class3. Бросает ошибку.


Стектрейс будет вида:
Возник эксепшен некоторого типа.
в class3.Get()
в class1.Method()


По цепочке вызова можно понять что и где произошло. Можно понять прочитав тип и сообщение ошибки. Если у вас и возниакют проблемы, то уж извините, но скорее всего проблема в вашем коде…

P.S. Расскажите о своём опыте, пожалуйста, возможно я ошибаюсь.
Действительно, ситуация реальная. Но что-то мне подсказывает, что это как-то слишком сферически и вакуумно. На практике обычно и ошибки более конкретные, да и стек трейс очень редко получается коротким.

Но как вывод для себя:
Использовать return await…; повсеместно не стоит, т.к. проблема не понять стектрейс — крайне мала, а создавать на каждый чих конечный автомат — не лучшая идея, с точки зрения производительности. Если писать красивый и понятный код, следовать банальным принципам в программирвоании — то проблема с непонимание сткетрейса пропадёт.

alhimik45, спасибо за пример!
создавать на каждый чих конечный автомат — не лучшая идея, с точки зрения производительности

Один запрос к БД нивелирует эти наносекунды выигрыша. Всё таки слишком уж premature optimization сразу лепить return Task и потенциально получать проблемы с разбором стектрейсов и дальнейшем написанием try-catch/using. Да и при наличии проблем с производительностью этот финт точно не в числе первых применяемых должен быть.

Ниже уже ответили за меня, по сути ситуация была примерно такая же, только перегрузок гораздо больше, и несколько таких слоёв в стек-трейсе (25-35 методов длиной). Всё это было замешано с обфускацией и TaskCompletionSource, в который устанавливали Exception и потом не await'или в некоторых ситуациях — соответственно когда GC собирал эту задачу — прилетал UnobservedTaskException с чудесным стеком. Я не говорю, что нереально разобраться — однако людское время дороже, чем машинное — если вы понимаете, о чём я.
Используйте Task.WaitAll, чтобы дождаться завершения всех задач.

Если, есть несколько тасков и нужно из каждого из них получить результат, то я бы не рекомендовал использовать Task.WaitAll, так как во многих случаях применение этой функции избыточно и ведет к более громоздкому коду.


Вот такого подхода обычно бывает достаточно:


Task<string> nameRequest = GetNameAsync();
Task<int> ageRequest = GetAgeAsync();
//Таски работают параллельно
string name = await nameRequest ;
int age = await ageRequest;
так как во многих случаях применение этой функции избыточно и ведет к более громоздкому коду.

Ответь на пацана, что код приведенный выше менее громоздкой чем:

var nameRequest = GetNameAsync();
var ageRequest = GetAgeAsync();

Task.WaitAll(nameRequest,ageRequest);


Или такой:
Task.WaitAll(GetNameAsync(),GetAgeAsync());
А еще WaitAll блокирующий. Ну или автор имел ввиду WhenAll.

Кстати да, этого я не заметил. Подозреваю, что в исходной статье опечатка.
Вот там такой комментарий есть:


Nice article, thanks! One concern: are you sure using WaitAny() / WaitAll() is a good solution for the last-mentioned case? I would suggest using WhenAny() / WhenAll() instead
Когда используется Task.ConfigureAwait(false), код больше не пытается возобновить с того места, где он был раньше
Что значит «не пытается возобновить»? Что-то вы не то говорите.
Скорее всего имелось ввиду что не пытается вернуться в тот SynchronizationContext к-рый был на момент вызова асинхронного метода. Само собой по окончанию выполнения вызываемого метода будет вызван «continuation» делегат
Если метод асинхронный, добавьте суффикс Async к его имени
Это имеет смысл, когда в классе/интерфейсе есть такие же синхронные методы, но бездумное добавление суффикса Async просто загрязняет код.
Для core советы рабочие или там иная схема работы?
//Проверка запрошена ли отмена
if (ct != null)

Но CancellationToken — это структура и она всегда будет не null. Или я где-то ошибся?
Немного дополню:
1. Отличный FAQ по ConfigureAwait — devblogs.microsoft.com/dotnet/configureawait-faq
2. Если асинхронные вызовы внутри метода поддерживают CancellationToken — обязательно добавляйте
, CancellationToken cancellationToken = default
последним параметром метода и передавайте его внутрь этих методов.
3. Для асинхронных методов интерфейсов всегда указывайте
, CancellationToken cancellationToken = default
как последний параметр.
4. При отмене с помощью CancellationToken ловить исключение отмены нужно с помощью OperationCanceledException — это базовый класс для всех исключений после отмены.
5. Если нужно добавить тайм-аут или отмену, используйте CancellationTokenSource в дополнение к уже переданному токену-аргументу с помощью такого кода:
using var registration = cancellationToken.Register(() => cancellationTokenSource.Cancel());

Этот код также отменит регистрацию после вызова Dispose()
6. В сложных асинхронных классах имеет смысл добавлять реализацию
IAsyncDisposable.DisposeAsync()
, если нужно что-то ждать для очищения ресурсов.
Использование возможно с помощью конструкции await using вместо обычного using.
await using var registration = cancellationTokenSource.Token.Register(() => completionSource.TrySetCanceled());

А чем CancellationTokenSource.CreateLinkedTokenSource не устроило?

Спасибо. Хотел узнать что-нибудь новое с помощью этой статьи — узнал
Работа, связанная с процессором: ваш код будет выполнять сложные вычисления. В этом случае вы должны использовать Async/Await, но запустить работу нужно в другом потоке с помощью Task.Run

А зачем делать CPU-bound операции асинхронными, все равно процессор будет занят именно этой операцией? Единственное, что приходит на ум — десктопное приложение, но это же только частный случай

Потому что вызывающий код будет ожидать, что ваш метод вернет управление быстро.


Например, там может быть написано что-то такое:


await Task.WhenAll(Enumerable.Range(0, n).Select(i => FooAsync(i)));

Если FooAsync решит по-считать что-то тяжелое в том же потоке — этот код отработает совершенно не так как задумывалось.

Видимо метод выглядит примерно так

Task FooAsync(int i)
{
    return Task.Run(() => 
    {
        // cpu-bound операция, зависящая от i
    });
}

Я не понял, что вы мне хотели сказать об асинхронности, приведя пример распараллеливания. В данном случае на каждый вызов числа из коллекции от 0 до n будет браться поток из пула и выполнение программы ускориться. Но если мы возьмем однократный вызов данного метода, это не даст никаких преимуществ, только лишний поток из пула будет выделен.
Лучше просто оставить так

void Foo(int i)
{
    // cpu-bound операция, зависящая от i
}

Вызвавшему потокe нужен результат выполнения, и ему все равно, сам он выполнит эту операцию или другой поток (в случае с десктопным приложением, действительно, будет предпочтительнее выделение потока, чтобы поток UI не блокировался). Асинхронная операция подразумевает, что процессору нечего делать, и пускай он другим чем-нибудь займется, а не будет просто ждать. В случае с cpu-bound процессору как раз есть чем заниматься
Сам недавно делал асинхронный отчёт о прогрессе, но получилось очень костыльно через события и Dispatcher. Спасибо за статью, пойду рефакторить)
Sign up to leave a comment.