Перехват исключений в асин c методах, когда не вызывается с помощью await - PullRequest
2 голосов
/ 01 апреля 2020

Цель:

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

Резюме

Я думал, что когда вызывается метод async, код в нем выполняется синхронно, пока не достигнет первого ожидания. Если это так, то , если во время этого «синхронного кода» выдается исключение, почему оно не распространяется до вызывающего метода? (Как и обычный синхронный метод.)

Пример кода:

С учетом следующего кода в. Net Базовом консольном приложении:

static void Main(string[] args)
{
    Console.WriteLine("Hello World!");

    try
    {
        NonAwaitedMethod();
    }
    catch (Exception e)
    {
        Console.WriteLine("Exception Caught");
    }

    Console.ReadKey();
}

public static async Task NonAwaitedMethod()
{
    Task startupDone = new Task(() => { });
    var runTask = DoStuff(() =>
    {
        startupDone.Start();
    });
    var didStartup = startupDone.Wait(1000);
    if (!didStartup)
    {
        throw new ApplicationException("Fail One");
    }

    await runTask;
}

public static async Task DoStuff(Action action)
{
    // Simulate starting up blocking
    var blocking = 100000;
    await Task.Delay(500 + blocking);
    action();
    // Do the rest of the stuff...
    await Task.Delay(3000);
}

}

Scenar ios:

  1. При запуске как есть этот код вызовет исключение, но, если у вас нет точки останова, вы будете не знаю это. Отладчик Visual Studio и консоль не сообщат о наличии проблемы (кроме примечания в одну строку на экране «Вывод»).

  2. Замените тип возврата NonAwaitedMethod на Task до void. Это приведет к тому, что отладчик Visual Studio перестанет работать с исключением. Он также будет распечатан в консоли. Но, в частности, исключение составляет NOT , обнаруженное в операторе catch, найденном в Main.

  3. Оставьте тип возврата NonAwaitedMethod как void , но сними async. Также измените последнюю строку с await runTask; на runTask.Wait(); (это по существу удаляет все асинхронные c вещи.) При запуске исключение перехватывается в операторе catch в методе Main.

Итак, подведем итог:

| Scenario   | Caught By Debugger | Caught by Catch |  
|------------|--------------------|-----------------|  
| async Task | No                 | No              |  
| async void | Yes                | No              |  
| void       | N/A                | Yes             |  

Вопросы:

Я думал, что это потому, что исключение было сгенерировано до того, как await было сделано что он будет выполняться синхронно до и через исключение.

Отсюда мой вопрос: Почему ни 10, ни 2 сценарий не попадают в catch оператор?

Кроме того, почему переключение с Task на void тип возвращаемого значения приводит к тому, что исключение отлавливается отладчиком? (Даже если я не использую этот тип возврата.)

Ответы [ 3 ]

4 голосов
/ 01 апреля 2020

Исключение было сгенерировано до того, как было выполнено ожидание, что оно будет выполняться синхронно

Думал, что это довольно верно, но это не значит, что вы можете перехватить исключение.

Поскольку в вашем коде есть ключевое слово async, которое превращает метод в асинхронный конечный автомат c, т. Е. Инкапсулированный / обернутый специальным типом. Любое исключение, генерируемое из конечного автомата asyn c, будет перехвачено и переброшено, когда задача await ed (кроме тех, async void) или они go ненаблюдаемые, что может быть зафиксировано в событии TaskScheduler.UnobservedTaskException .

Если вы удалите ключевое слово async из метода NonAwaitedMethod, вы можете перехватить исключение.

Хороший способ наблюдать за этим поведением - использовать следующее:

try
{
    NonAwaitedMethod();

    // You will still see this message in your console despite exception
    // being thrown from the above method synchronously, because the method
    // has been encapsulated into an async state machine by compiler.
    Console.WriteLine("Method Called");
}
catch (Exception e)
{
    Console.WriteLine("Exception Caught");
}

Итак, ваш код скомпилирован аналогично следующему:

try
{
    var stateMachine = new AsyncStateMachine(() =>
    {
        try
        {
            NonAwaitedMethod();
        }
        catch (Exception ex)
        {
            stateMachine.Exception = ex;
        }
    });

    // This does not throw exception
    stateMachine.Run();
}
catch (Exception e)
{
    Console.WriteLine("Exception Caught");
}

, почему переключение с Task на void с возвращаемым типом приводит к тому, что исключение будет поймано

Если Метод возвращает Task, исключение перехватывается задачей.

Если метод void, то исключение повторно генерируется из потока произвольного пула потоков. Любое необработанное исключение, выброшенное из потока пула потоков, приведет к тому, что приложение обработает sh, так что скорее всего отладчик (или, возможно, отладчик JIT) наблюдает за такого рода исключениями.

Если вы хотите запустить и забыть но правильно обработав исключение, вы можете использовать ContinueWith для создания продолжения задачи:

NonAwaitedMethod()
    .ContinueWith(task => task.Exception, TaskContinuationOptions.OnlyOnFaulted);

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

Или, если исключение необходимо перехватить и обработать в Main, правильный способ сделать это - использовать asyn c Основные методы .

3 голосов
/ 02 апреля 2020

Если во время этого «синхронного кода» возникает исключение, почему оно не распространяется до вызывающего метода? (Как и обычный синхронный метод.)

Хороший вопрос. И действительно, ранние предварительные версии async / await имели такое поведение. Но языковая команда решила, что поведение было слишком запутанным.

Это достаточно легко понять, когда у вас есть такой код:

if (test)
  throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));

А как же код, подобный этому:

await Task.Delay(1);
if (test)
  throw new Exception();
await Task.Delay(TaskSpan.FromSeconds(5));

Помните, что await действует синхронно , если его ожидание уже завершено. Итак, прошло ли 1 миллисекунда к тому времени, когда ожидается задание, возвращенное с Task.Delay? Или для более реалистичного примера c, что происходит, когда HttpClient возвращает локально кэшированный ответ (синхронно)? В более общем смысле, прямой выброс исключений во время синхронной части метода имеет тенденцию приводить к коду, который меняет свою семантику в зависимости от условий гонки.

Таким образом, было принято решение в одностороннем порядке изменить все async методы работают так, что все сгенерированные исключения помещаются в возвращаемое задание. Как хороший побочный эффект, это приводит их семантику в соответствие с блоками перечислителя; если у вас есть метод, который использует yield return, любые исключения не будут видны, пока перечислитель не будет реализован , а не когда метод называется .

Относительно вашего scenar ios:

  1. Да, исключение игнорируется. Поскольку код в Main выполняет «запустить и забыть», игнорируя задачу. А «уволить и забыть» означает «мне наплевать на исключения». Если вы действительно заботитесь об исключениях, не используйте "огонь и забудьте"; вместо этого await задача в какой-то момент. Задача состоит в том, как async методы сообщают о своем завершении своим вызывающим, а await - как вызывающий код получает результаты задачи (и наблюдает исключения) .
  2. Да , async void - странная причуда (и вообще следует избегать ). Он был добавлен в язык для поддержки асинхронных обработчиков событий, поэтому он имеет семантику, аналогичную обработчикам событий. В частности, любые исключения, которые выходят за пределы метода async void, возникают в контексте верхнего уровня, который был текущим в начале метода. Вот как исключения также работают для обработчиков событий пользовательского интерфейса. В случае консольного приложения исключения возникают в потоке пула потоков. Обычные async методы возвращают «дескриптор», который представляет асинхронную операцию и может содержать исключения. Исключения из async void методов не могут быть перехвачены, так как для этих методов нет «дескриптора».
  3. Ну, конечно. В этом случае метод является синхронным, и исключения перемещаются вверх по стеку, как обычно.

Что касается примечания, никогда, никогда не используйте конструктор Task . Если вы хотите запустить код в пуле потоков, используйте Task.Run. Если вы хотите иметь асинхронный тип делегата, используйте Func<Task>.

1 голос
/ 02 апреля 2020

Ключевое слово async указывает, что компилятор должен преобразовать метод в асинхронный конечный автомат c, который не настраивается в отношении обработки исключений. Если вы хотите, чтобы syn c -part-exception метода NonAwaitedMethod генерировался немедленно, нет другого варианта, кроме удаления ключевого слова async из метода. Вы можете получить лучшее из обоих миров, переместив асин c часть в асин c локальную функцию :

public static Task NonAwaitedMethod()
{
    Task startupDone = new Task(() => { });
    var runTask = DoStuff(() =>
    {
        startupDone.Start();
    });
    var didStartup = startupDone.Wait(1000);
    if (!didStartup)
    {
        throw new ApplicationException("Fail One");
    }

    return ImlpAsync(); async Task ImlpAsync()
    {
        await runTask;
    };
}

Вместо использования именованной функции, вы также можете используйте анонимный:

return ((Func<Task>)(async () =>
{
    await runTask;
}))();
...