Ловіть виняток, викинутий методом анулювання анулювання


283

Використовуючи асинхронний CTP від ​​Microsoft для .NET, чи можна зловити виняток, викинутий методом async в методі виклику?

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. */
    }
}

Отже, я хочу, щоб виняток з коду асинхронізації міхур мінявся в мій код виклику, якщо це взагалі можливо.



22
Якщо хтось натрапляє на це в майбутньому, стаття Async / Await Best Practices ... добре пояснює це в "Малюнок 2. Винятки з методу асинхнозної пустоти не можуть бути спіймані". " Коли викид викидається з методу Async Task або Async Task <T>, цей виняток захоплюється і розміщується на об'єкті Task. За допомогою методів асинхронізації не існує об'єкта Task, будь-які винятки, викинуті з методу анулювання анулювання буде піднято безпосередньо на SynchronizationContext, який був активним, коли почався метод асинхронізації. "
Містер Муз

Ви можете скористатися таким підходом або цим
Целофан

Відповіді:


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. */
    }
} 

Методи асинхронізації недійсних мають різну семантику обробки помилок. Коли виняток викидається з завдання асинхронізації або методу асинхронної задачі, цей виняток фіксується та розміщується на об’єкті Завдання. У методах анулювання анулювання немає об’єкта Task, тому будь-які винятки, викинуті з методу анулювання анулювання, будуть підняті безпосередньо на синхронізаціїContext, яка була активною при запуску методу анулювання анулювання. - 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 () повинен бути позначений як Завдання замість недійсним.
Сорній

4
Я майже впевнений, що це призведе до агрегації. Таким чином, блок ловлі, який відображається у цій відповіді, не буде винятком.
xanadont

2
", але тільки якщо ви очікуєте або зачекаєте () дзвінка на Фоо" Як ви можете awaitзателефонувати до Foo, коли Foo повернеться недійсним? async void Foo(). Type void is not awaitable?
підйом

3
Не можете чекати методу недійсності, чи не так?
Хітеш П

74

Причина, за якою виняток не вловлюється, полягає в тому, що метод Foo () має тип недійсного повернення, і коли викликається очікування, він просто повертається. Оскільки 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). Якщо ви поставите спробу / ловити навколо вашого методу очікування, ви можете "зловити" виняток звичайним способом, хоча ваш код виконується на іншій нитці, коли завдання обчислення закінчено і ваше примикання виконано.

Метод обчислення автоматично відстежує викинутий виняток, оскільки я використовував ApiChange.Api.dll з інструменту ApiChange . Трасування та рефлектор багато допомагають зрозуміти, що відбувається. Щоб позбутися від нарізки, ви можете створити власні версії GetAwaiter BeginAwait і EndAwait і обернути не завдання, а, наприклад, Ледачий та простежити всередині власних методів розширення. Тоді ви отримаєте набагато краще розуміння того, що компілятор і що робить TPL.

Тепер ви бачите, що немає можливості повернутися до випробування / зловити виняток, оскільки не існує жодного кадру стека для жодного винятку, з якого можна поширитись. Ваш код може робити щось зовсім інше після того, як ви ініціювали операції з асинхронізацією. Це може викликати Thread.Sleep або навіть припинити. Поки залишився один ниток переднього плану, ваша програма із задоволенням продовжить виконувати асинхронні завдання.


Ви можете обробити виняток всередині методу async після того, як ваша асинхронна операція закінчилася, і передзвонити в потік інтерфейсу користувача. Рекомендований спосіб зробити це за допомогою 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

Також важливо зауважити, що ви втратите хронологічний слід стека винятку, якщо у вас є метод повернення недійсним методом асинхронізації. Я б рекомендував повернути завдання так. Зробити налагодження цілком багато.

public async Task DoFoo()
    {
        try
        {
            return await Foo();
        }
        catch (ProtocolException ex)
        {
            /* Exception with chronological stack trace */     
        }
    }

Це спричинить проблему з не всіма шляхами, що повертають значення, оскільки, якщо є виняток, значення не повертається, тоді як у спробі є. Якщо у вас немає returnзаяви, цей код працює, оскільки Task"неявно" повертається за допомогою async / await.
Matias Grioni

2

Цей блог акуратно пояснює вашу проблему Async Best Practices .

Суть у тому, що ви не повинні використовувати void як повернення для асинхронного методу, якщо тільки це не обробник подій async, це погана практика, оскільки він не дозволяє виловлювати винятки ;-).

Найкращою практикою було б змінити тип повернення на Завдання. Крім того, спробуйте кодувати асинхронізацію до кінця, здійснити виклик кожного методу асинхронізації та викликати з асинхронних методів. За винятком основного методу в консолі, який не може бути асинхронізований (до C # 7.1).

Якщо ви проігноруєте цю найкращу практику, ви зіткнетесь з тупиками із програмами GUI та ASP.NET. Тупик виникає через те, що ці програми працюють у контексті, який дозволяє лише один потік і не буде віддавати його до потоку асинхронізації. Це означає, що графічний інтерфейс чекає синхронно на повернення, тоді як метод асинхронізації очікує контексту: тупик.

Така поведінка не буде відбуватися в консольному додатку, оскільки вона працює в контексті з пулом потоків. Метод асинхронізації повернеться в інший потік, який буде заплановано. Ось чому тест-консольний додаток буде працювати, але ті ж виклики стануть тупиком в інших додатках ...


1
"За винятком основного методу в консолі, який не може бути асинхронізований." Оскільки C # 7.1, Main тепер може бути посиланням
Адам
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.