Приложение закрывается даже при исключениях из обращения - PullRequest
0 голосов
/ 03 февраля 2020

У меня есть действие, инициируемое кнопкой, которая должна охватывать все возможные случаи.

private async void btnStart_Click(object sender, EventArgs e)
{
    try
    {
        btnStart.Enabled = false;
        await Task.Delay(1000);
        btnStart.Visible = false;
        btnStop.Visible = true;
        var maxSessions = numericFieldSessions.Value;//to run the same stuff in parallell
        for (var i = 0; i < maxSessions; i++)
        {
            await Task.Run(() =>
            {
                Parallel.Invoke(async () =>
                {
                    while (true)
                    {
                        try
                        {
                            A();
                            await Task.Run(() => { B(); }); //longer operation
                        }
                        catch (CustomExceptionA ex)
                        {
                            DoLog($"Custom Exception A: {ex.Message}");
                        }
                        catch (CustomExceptionB ex)
                        {
                            DoLog($"Custom Exception B: {ex.Message}");
                        }
                        catch (CustomExceptionC ex)
                        {
                            DoLog($"Custom Exception C: {ex.Message}");
                        }
                        catch (Exception ex)
                        {
                            DoLog($"Generic Exception: {ex.Message}");
                        }
                    }
                });

            });
        }
    }
    catch (Exception ex)
    {
        DoLog($"Full Generic Exception: {ex.Message}");
    }
}

DoLog() только записывает строку в файл. Через долгое время программа просто кра sh. Без регистрации ничего. В журнале событий Windows я увидел, что необработанное исключение было сгенерировано внутри метода B(). Но B() само по себе не должно обрабатывать ошибки ... и это не так!

Это журнал:

System.Runtime.InteropServices.ExternalException
   em System.Drawing.Image.FromHbitmap(IntPtr, IntPtr)
   em System.Drawing.Image.FromHbitmap(IntPtr)
   em System.Drawing.Icon.BmpFrame()
   em System.Drawing.Icon.ToBitmap()
   em System.Windows.Forms.ThreadExceptionDialog..ctor(System.Exception)
   em System.Windows.Forms.Application+ThreadContext.OnThreadException(System.Exception)
   em System.Windows.Forms.Control.WndProcException(System.Exception)
   em System.Windows.Forms.Control+ControlNativeWindow.OnThreadException(System.Exception)
   em System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)

И сразу после этого события ошибки есть другое (в в ту же секунду):

Faulting application name: MyApp.exe, version: 1.0.0.0, timestamp: 0xb5620f2c
   Faulty module name: KERNELBASE.dll, version: 10.0.18362.476, timestamp: 0x540698cd
   Exception code: 0xe0434352
   Fault offset: 0x001135d2
   Failed process ID: 0xf54
   Failed application start time: 0x01d5da61843fe0f8
   Faulting application path: PATH_TO_MY_APP.exe
   Faulting module path: C:\Windows\System32\KERNELBASE.dll
   Report ID: 120a68ca-a077-47a4-ae62-213e146956a6
   Failed package full name:
   Application ID for the failed package:

Как это предотвратить? Я думал, что каждое исключение будет обработано .. как предотвратить это? Предполагая, что - что-нибудь, что происходит внутри B(), должно обрабатываться вне его?

1 Ответ

0 голосов
/ 06 февраля 2020

С Пост Питера Торра в Asyn c и Исключения в C# он предлагает следующее предложение при обработке исключений в асин c методах:

По сути, для обеспечения безопасности вам необходимо выполнить одно из двух действий:

  1. Обработка исключений внутри самого метода asyn c ; или
  2. Возвратите задачу и убедитесь, что вызывающая сторона пытается получить результат, одновременно обрабатывая исключения (возможно, в родительском стековом кадре)

Невыполнение любого из этих действий приведет к в нежелательном поведении.

Воспроизведение ошибки

Поскольку я не знаю метод 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.");
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...