С Пост Питера Торра в Asyn c и Исключения в C# он предлагает следующее предложение при обработке исключений в асин c методах:
По сути, для обеспечения безопасности вам необходимо выполнить одно из двух действий:
- Обработка исключений внутри самого метода asyn c ; или
- Возвратите задачу и убедитесь, что вызывающая сторона пытается получить результат, одновременно обрабатывая исключения (возможно, в родительском стековом кадре)
Невыполнение любого из этих действий приведет к в нежелательном поведении.
Воспроизведение ошибки
Поскольку я не знаю метод sugnature вашего b
метода, я начал с основы c void b()
. Используя void b()
Мне не удалось воспроизвести вашу ошибку в следующем фрагменте:
private async void button1_Click(object sender, EventArgs e)
{
try
{
await Task.Run(() => { b(); });
//also tried:
//await Task.Run(b);
//await Task.Run(new Action(b));
}
catch (Exception E)
{
MessageBox.Show($"Exception Handled: \"{E.Message}\"");
}
}
void b()
{
DateTime begin = DateTime.Now;
while (DateTime.Now.Subtract(begin).TotalSeconds < 3) //Wait 3 seconds
{ /*Do Nothing*/ }
//c() represents whichever method you're calling
//inside of b that is throwing the exception.
c();
}
void c()
{
throw new Exception("Try to handle this exception.");
}
В этом случае VS прервался, когда выбрасывалось исключение, выделяя строку выброса, утверждая, что это исключение, не обработанное пользователем, однако при продолжающемся выполнении перехватило исключение, и в окне сообщения было показано . Выполнение примера без отладчика не вызвало разрывов, и MessageBox был показан, как и ожидалось.
Позже я попытался изменить метод b и сделать его async void
:
async void b()
{
await Task.Run(() =>
{
DateTime begin = DateTime.Now;
while (DateTime.Now.Subtract(begin).TotalSeconds < 10) //Wait 10 seconds
{ /*Do Nothing*/ }
});
c();
}
В этом В сценарии, где b
равно async
, я смог воспроизвести вашу ошибку. Отладка в Visual Studio по-прежнему информирует меня об исключении, как только оно выдается, выделяя строку выброса, однако, продолжающееся выполнение теперь прерывает программу, и блок try-catch
не смог перехватить исключение.
Это вероятно, это происходит потому, что async void
определяет шаблон "Fire-and-Forget". Даже если вы вызываете его через Task.Run()
, await
до Task.Run()
НЕ , получая результат b()
, потому что он все еще пуст. Это приводит к тому, что Исключение остается неиспользованным до тех пор, пока G C не попытается его собрать
По словам Питера Торра:
Основная причина c в том, что если вы не пытайтесь получить результат Task (либо используя await , либо получая Result напрямую), тогда он просто сидит там, держась за объект исключения, ожидающий получения GCed. Во время G C он замечает, что никто никогда не проверял результат (и, следовательно, никогда не видел исключения), и поэтому рассматривает его как ненаблюдаемое исключение. Как только кто-то запрашивает результат, Task выдает исключение, которое затем должно быть кем-то перехвачено.
Решение
Что решило проблему для меня было изменение подписи void b()
на async Task b()
, также, после этого изменения, вместо того, чтобы звонить с b
через Task.Run()
, теперь вы можете просто вызвать ее напрямую с помощью await b();
(см. Решение 1 ниже).
Если у вас есть доступ к реализации b, но по какой-то причине вы не можете изменить его сигнатуру (например, для обеспечения обратной совместимости), вам придется используйте блок try-catch внутри b, но вы не можете перебрасывать любые перехваченные исключения, или такая же ошибка будет продолжаться (см. Решение 2 ниже).
Решение 1
Изменить подпись b:
private async void button1_Click(object sender, EventArgs e)
{
//Now any exceptions thrown inside b, but not handled by it
//will properly move up the call stack and reach this level
//where this try-catch block will be able to handle it.
try
{
await b();
}
catch (Exception E)
{
MessageBox.Show($"Exception Handled: \"{E.Message}\"");
}
}
async Task b()
{
await Task.Run(()=>
{
DateTime begin = DateTime.Now;
while (DateTime.Now.Subtract(begin).TotalSeconds < 3) //Wait 3 seconds
{ /*Do Nothing*/ }
});
c();
}
void c()
{
throw new Exception("Try to handle this exception.");
}
Решение 2
Изменить тело b:
private async void button1_Click(object sender, EventArgs e)
{
//With this solution, exceptions are treated inside b's body
//and it will not rethrow the exception, so encapsulating the call to b()
//in a try-catch block is redundant and unecessary, since it will never
//throw an exception to be caught in this level of the call stack.
await Task.Run(() => { b(); });
}
void b()
{
DateTime begin = DateTime.Now;
while (DateTime.Now.Subtract(begin).TotalSeconds < 3) //Wait 3 seconds
{ /*Do Nothing*/ }
try
{
c();
}
catch (Exception)
{
//Log the error here.
//DO NOT re-throw the exception.
}
}
void c()
{
throw new Exception("Try to handle this exception.");
}