Почему Exception from async void приводит к сбою приложения, но из async Task проглатывается - PullRequest
0 голосов
/ 08 ноября 2018

Я понимаю, что async Task исключения могут быть обнаружены:

try { await task; }
catch { }

в то время как async void не может, потому что его нельзя ожидать.

Но почему асинхронное задание не ожидается (как асинхронное void one), то Exception проглатывается, а void у кого вылетает приложение?

Звонящий : ex();

Вызывается

async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }

Ответы [ 3 ]

0 голосов
/ 12 ноября 2018

TL; DR

Это потому, что async void не должно использоваться! async void только для того, чтобы заставить работать старый код (например, обработчики событий в WindowsForms и WPF).

Технические данные

Это из-за того, как компилятор C # генерирует код для async методов.

Вы должны знать, что за async / await находится конечный автомат (реализация IAsyncStateMachine), сгенерированный компилятором.

Когда вы объявляете метод async, для него будет создан конечный автомат struct. Для вашего ex() метода этот код конечного автомата будет выглядеть так:

void IAsyncStateMachine.MoveNext()
{
    try
    {
        throw new Exception();
    }
    catch (Exception exception)
    {
        this.state = -2;
        this.builder.SetException(exception);
    }
}

Обратите внимание, что this.builder.SetException(exception); оператор. Для Task -возвратного async метода это будет AsyncTaskMethodBuilder объект. Для метода void ex() это будет AsyncVoidMethodBuilder.

Тело метода ex() будет заменено компилятором на что-то вроде этого:

private static Task ex()
{
    ExAsyncStateMachine exasm;
    exasm.builder = AsyncTaskMethodBuilder.Create();
    exasm.state = -1;
    exasm.builder.Start<ExAsyncStateMachine>(ref exasm);
    return exasm.builder.Task;
}

(а для async void ex() не будет последней return строки)

Метод Start<T> построителя метода вызовет метод MoveNext конечного автомата. Метод конечного автомата перехватывает исключение в своем блоке catch. Это исключение обычно следует соблюдать для объекта Task - метод AsyncTaskMethodBuilder.SetException сохраняет этот объект исключения в экземпляре Task. Когда мы отбрасываем этот экземпляр Task (без await), мы вообще не видим исключение, но само исключение больше не генерируется.

В конечном автомате для async void ex() вместо этого есть AsyncVoidMethodBuilder. Его метод SetException выглядит иначе: поскольку Task не где хранить исключение, его нужно выбросить. Это происходит по-другому, но не просто throw:

AsyncMethodBuilderCore.ThrowAsync(exception, synchronizationContext);

Логика внутри этого AsyncMethodBuilderCore.ThrowAsync помощника решает:

  • Если есть SynchronizationContext (например, мы находимся в потоке пользовательского интерфейса приложения WPF), исключение будет опубликовано в этом контексте.
  • В противном случае исключение будет помещено в очередь в потоке ThreadPool.

В обоих случаях исключение не будет перехвачено блоком try-catch, который может быть установлен вокруг вызова ex() (если у вас нет специального SynchronizationContext, который может это сделать, см., Например, Стивена Клири AsyncContext).

Причина проста: когда мы отправляем a throw действие или ставим в очередь его, мы просто возвращаемся из метода ex() и таким образом покидаем блок try-catch , Затем выполняется опубликованное / поставленное в очередь действие (в том же или в другом потоке).

0 голосов
/ 18 ноября 2018

Пожалуйста, прочитайте важную заметку внизу.

Метод async void приведет к аварийному завершению работы приложения, поскольку для компилятора C # нет объекта Task, в который было бы помещено исключение. На функциональном уровне ключевое слово async в методе Task - просто тяжелый синтаксический сахар, который говорит компилятору переписать ваш метод в терминах объекта Task, используя различные методы, доступные для объекта, а также такие утилиты, как Task.FromResult, Task.FromException и Task.FromCancelled или иногда Task.Run, или эквиваленты с точки зрения компилятора. Это означает, что код такой:

async Task Except()
{
    throw new Exception { };
}

превращается в приблизительно :

Task Except()
{
    return Task.FromException(new Exception { });
}

и так, когда вы вызываете Task -возврат async методов, которые throw, программа не падает, потому что на самом деле не выдается исключение; вместо этого объект Task создается в «исключенном» состоянии и возвращается вызывающей стороне. Как упоминалось ранее, метод, декорированный async void, не имеет возвращаемого объекта Task, поэтому компилятор не пытается переписать метод в терминах объекта Task, а вместо этого пытается только иметь дело с получение значений ожидаемых звонков.

Больше контекста

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

Task Except() // Take note that there is no async modifier present.
{
    throw new Exception { }; // This will now throw no matter what.
    return Task.FromResult(0); // Task<T> derives from Task so this is an implicit cast.
}

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

Важное примечание

Способ, которым эти «переписывания» фактически обрабатываются компилятором и проявляется скомпилированным кодом, может отличаться от того, что я подразумевал, но примерно эквивалентен на функциональном уровне.

0 голосов
/ 08 ноября 2018

Потому что ваши методы не выполняются асинхронно.

Выполнение будет выполняться синхронно до тех пор, пока оно не «встретится» с ключевым словом await.

Таким образом, в случае void приложение сгенерирует исключение, поскольку исключение возникает в текущем контексте выполнения.

В случае Task даже исключения, генерируемого синхронно, оно будет помещено в Task и возвращено вызывающей стороне.

Вы также должны получить желаемое поведение с void, если будете использовать await в функции.

async void Ex()
{
    await Task.Delay(1000);
    throw new Exception();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...