Поймать исключение, выданное асинхронным пустым методом

283

Используя асинхронную CTP от Microsoft для .NET, возможно ли перехватить исключение, вызванное асинхронным методом в вызывающем методе?

public async void Foo()
{
    var x = await DoSomethingAsync();

    /* Handle the result, but sometimes an exception might be thrown.
       For example, DoSomethingAsync gets data from the network
       and the data is invalid... a ProtocolException might be thrown. */
}

public void DoFoo()
{
    try
    {
        Foo();
    }
    catch (ProtocolException ex)
    {
          /* The exception will never be caught.
             Instead when in debug mode, VS2010 will warn and continue.
             The deployed the app will simply crash. */
    }
}

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

TimothyP
источник
1
Это вам помогает? social.msdn.microsoft.com/Forums/en/async/thread/...
svrist
22
На случай, если кто-нибудь наткнется на это в будущем, статья Async / Await Best Practices ... имеет хорошее объяснение этого в «Рисунке 2 Исключения из метода Async Void нельзя поймать с помощью Catch». « Когда исключение выбрасывается из асинхронной задачи или метода асинхронной задачи <T>, это исключение захватывается и помещается в объект задачи. При использовании асинхронных пустых методов нет объекта« Задача », любые исключения, выбрасываемые из асинхронного пустого метода будет вызван непосредственно в SynchronizationContext, который был активен при запуске асинхронного пустого метода. "
Мистер Мус
Вы можете использовать этот подход или это
Tselofan

Ответы:

263

Это немного странно читать, но да, исключение будет пузыриться в вызывающем коде - но только если вы awaitили Wait()вызовFoo .

public async Task Foo()
{
    var x = await DoSomethingAsync();
}

public async void DoFoo()
{
    try
    {
        await Foo();
    }
    catch (ProtocolException ex)
    {
          // The exception will be caught because you've awaited
          // the call in an async method.
    }
}

//or//

public void DoFoo()
{
    try
    {
        Foo().Wait();
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught because you've
             waited for the completion of the call. */
    }
} 

Асинхронные методы void имеют различную семантику обработки ошибок. Когда исключение выбрасывается из метода асинхронной задачи или асинхронной задачи, это исключение захватывается и помещается в объект задачи. При использовании асинхронных void-методов объект Task отсутствует, поэтому любые исключения, выбрасываемые из асинхронного void-метода, будут вызываться непосредственно в SynchronizationContext, который был активен при запуске асинхронного void-метода. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Обратите внимание, что использование Wait () может привести к блокировке вашего приложения, если .Net решит выполнить ваш метод синхронно.

Это объяснение http://www.interact-sw.co.uk/iangblog/2010/11/01/csharp5-async-exceptions довольно хорошее - оно описывает шаги, которые компилятор предпринимает для достижения этой магии.

Стюарт
источник
3
Я на самом деле имею в виду, что читать прямо - хотя я знаю, что на самом деле все происходит действительно сложно - поэтому мой мозг говорит мне не верить своим глазам ...
Стюарт
8
Я думаю, что метод Foo () должен быть помечен как Task, а не void.
Сорний
4
Я уверен, что это приведет к AggregateException. Таким образом, блок catch, представленный в этом ответе, не будет перехватывать исключение.
xanadont
2
«но только если вы ждете или подождите () вызов Foo» Как вы можете awaitпозвонить в Foo, когда Foo возвращает void? async void Foo(), Type void is not awaitable?
RISM
3
Не может ждать пустого метода, не так ли?
Hitesh P
74

Причина, по которой исключение не перехвачено, состоит в том, что метод Foo () имеет возвращаемый тип void, поэтому при вызове await он просто возвращается. Поскольку DoFoo () не ожидает завершения Foo, обработчик исключений не может быть использован.

Это открывает более простое решение, если вы можете изменить сигнатуры метода - измените Foo()так, чтобы он возвращал тип, Taskа затем DoFoo()мог await Foo(), как в этом коде:

public async Task Foo() {
    var x = await DoSomethingThatThrows();
}

public async void DoFoo() {
    try {
        await Foo();
    } catch (ProtocolException ex) {
        // This will catch exceptions from DoSomethingThatThrows
    }
}
Роб Черч
источник
19
Это действительно может подкрасться к вам и должно быть предупреждено компилятором.
GGleGrand
19

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

Код ниже делает следующее:

  • Создать 4 задачи
  • Каждая задача будет асинхронно увеличивать число и возвращать увеличенное число
  • Когда наступает асинхронный результат, он отслеживается.

 

static TypeHashes _type = new TypeHashes(typeof(Program));        
private void Run()
{
    TracerConfig.Reset("debugoutput");

    using (Tracer t = new Tracer(_type, "Run"))
    {
        for (int i = 0; i < 4; i++)
        {
            DoSomeThingAsync(i);
        }
    }
    Application.Run();  // Start window message pump to prevent termination
}


private async void DoSomeThingAsync(int i)
{
    using (Tracer t = new Tracer(_type, "DoSomeThingAsync"))
    {
        t.Info("Hi in DoSomething {0}",i);
        try
        {
            int result = await Calculate(i);
            t.Info("Got async result: {0}", result);
        }
        catch (ArgumentException ex)
        {
            t.Error("Got argument exception: {0}", ex);
        }
    }
}

Task<int> Calculate(int i)
{
    var t = new Task<int>(() =>
    {
        using (Tracer t2 = new Tracer(_type, "Calculate"))
        {
            if( i % 2 == 0 )
                throw new ArgumentException(String.Format("Even argument {0}", i));
            return i++;
        }
    });
    t.Start();
    return t;
}

Когда вы наблюдаете следы

22:25:12.649  02172/02820 {          AsyncTest.Program.Run 
22:25:12.656  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.657  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 0    
22:25:12.658  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.659  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.659  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 1    
22:25:12.660  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 2    
22:25:12.662  02172/02820 {          AsyncTest.Program.DoSomeThingAsync     
22:25:12.662  02172/02820 Information AsyncTest.Program.DoSomeThingAsync Hi in DoSomething 3    
22:25:12.664  02172/02756          } AsyncTest.Program.Calculate Duration 4ms   
22:25:12.666  02172/02820          } AsyncTest.Program.Run Duration 17ms  ---- Run has completed. The async methods are now scheduled on different threads. 
22:25:12.667  02172/02756 Information AsyncTest.Program.DoSomeThingAsync Got async result: 1    
22:25:12.667  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 8ms    
22:25:12.667  02172/02756 {          AsyncTest.Program.Calculate    
22:25:12.665  02172/05220 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 0   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.668  02172/02756 Exception   AsyncTest.Program.Calculate Exception thrown: System.ArgumentException: Even argument 2   
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     
22:25:12.724  02172/05220          } AsyncTest.Program.Calculate Duration 66ms      
22:25:12.724  02172/02756          } AsyncTest.Program.Calculate Duration 57ms      
22:25:12.725  02172/05220 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 0  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 106    
22:25:12.725  02172/02756 Error       AsyncTest.Program.DoSomeThingAsync Got argument exception: System.ArgumentException: Even argument 2  

Server stack trace:     
   at AsyncTest.Program.c__DisplayClassf.Calculateb__e() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 124   
   at System.Threading.Tasks.Task`1.InvokeFuture(Object futureAsObj)    
   at System.Threading.Tasks.Task.InnerInvoke()     
   at System.Threading.Tasks.Task.Execute()     

Exception rethrown at [0]:      
   at System.Runtime.CompilerServices.TaskAwaiter.EndAwait()    
   at System.Runtime.CompilerServices.TaskAwaiter`1.EndAwait()  
   at AsyncTest.Program.DoSomeThingAsyncd__8.MoveNext() in C:\Source\AsyncTest\AsyncTest\Program.cs:line 0      
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 70ms   
22:25:12.726  02172/02756          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   
22:25:12.726  02172/05220 {          AsyncTest.Program.Calculate    
22:25:12.726  02172/05220          } AsyncTest.Program.Calculate Duration 0ms   
22:25:12.726  02172/05220 Information AsyncTest.Program.DoSomeThingAsync Got async result: 3    
22:25:12.726  02172/05220          } AsyncTest.Program.DoSomeThingAsync Duration 64ms   

Вы заметите, что метод Run завершается в потоке 2820, в то время как завершен только один дочерний поток (2756). Если вы применили метод try / catch к своему методу await, вы можете «перехватить» исключение обычным способом, хотя ваш код выполняется в другом потоке, когда задача вычисления завершена и ваша контикация выполнена.

Метод вычисления отслеживает выброшенное исключение автоматически, потому что я использовал ApiChange.Api.dll из инструмента ApiChange . Tracing and Reflector очень помогает понять, что происходит. Чтобы избавиться от многопоточности, вы можете создать свои собственные версии GetAwaiter BeginAwait и EndAwait и обернуть не задачу, а, например, Lazy и трассировку внутри ваших собственных методов расширения. Тогда вы получите гораздо лучшее понимание того, что делает компилятор и что делает TPL.

Теперь вы видите, что нет никакого способа получить / поймать ваше исключение назад, так как не осталось ни одного стекового фрейма для распространения какого-либо исключения. Ваш код может делать что-то совершенно другое после того, как вы инициировали асинхронные операции. Это может вызвать Thread.Sleep или даже прекратить. Пока остается один поток переднего плана, ваше приложение будет продолжать успешно выполнять асинхронные задачи.


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

Алоис Краус
источник
5

Исключение можно перехватить в асинхронной функции.

public async void Foo()
{
    try
    {
        var x = await DoSomethingAsync();
        /* Handle the result, but sometimes an exception might be thrown
           For example, DoSomethingAsync get's data from the network
           and the data is invalid... a ProtocolException might be thrown */
    }
    catch (ProtocolException ex)
    {
          /* The exception will be caught here */
    }
}

public void DoFoo()
{
    Foo();
}
Сандживакумар Хиремат
источник
2
Эй, я знаю, но мне действительно нужна эта информация в DoFoo, чтобы я мог отображать информацию в пользовательском интерфейсе. В этом случае для пользовательского интерфейса важно отобразить исключение, поскольку это не инструмент конечного пользователя, а инструмент для отладки протокола связи
TimothyP
В этом случае обратные вызовы имеют большой смысл. (Старые добрые асинхронные делегаты)
Sanjeevakumar Hiremath
@Tim: Включите всю необходимую информацию в выданное исключение?
Эрик Дж.
5

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

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }
rohanjansen
источник
Это вызовет проблему, при которой не все пути возвращают значение, поскольку, если есть исключение, значение не возвращается, а в попытке есть. Если у вас нет returnзаявления, этот код работает, однако, так как Task"неявно" возвращается с помощью async / await.
Матиас Гриони
2

Этот блог объясняет вашу проблему аккуратно в Async Best Practices .

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

Рекомендуется изменить тип возвращаемого значения на Task. Кроме того, попробуйте полностью закодировать асинхронный код, сделать каждый вызов асинхронного метода и вызываться из асинхронных методов. За исключением метода Main в консоли, который не может быть асинхронным (до C # 7.1).

Вы столкнетесь с тупиковыми ситуациями с приложениями с графическим интерфейсом и ASP.NET, если проигнорируете эту рекомендацию. Взаимная блокировка возникает из-за того, что эти приложения работают в контексте, который допускает только один поток и не передает его асинхронному потоку. Это означает, что GUI ожидает синхронно возврата, а асинхронный метод ожидает context: deadlock.

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

Стефан Гекьер
источник
1
«За исключением метода Main в консоли, который не может быть асинхронным». Начиная с C # 7.1, Main теперь может быть ссылкой
Адам