Предотвращение исключения внешнего исключения при броске из BeginInvoke
У меня есть обработчик Application.ThreadException
, но я обнаружил, что исключения не всегда правильно передаются ему. В частности, если я выкидываю исключение-with-inner-exception из обратного вызова BeginInvoke
, мой обработчик ThreadException
не получает внешнее исключение - он получает только внутреннее исключение.
Пример кода:
public Form1()
{
InitializeComponent();
Application.ThreadException += (sender, e) =>
MessageBox.Show(e.Exception.ToString());
}
private void button1_Click(object sender, EventArgs e)
{
var inner = new Exception("Inner");
var outer = new Exception("Outer", inner);
//throw outer;
BeginInvoke(new Action(() => { throw outer; }));
}
Если я раскомментирую строку throw outer;
и нажмите кнопку, то в окне сообщений отображается внешнее исключение (вместе со своим внутренним исключением):
System.Exception: Outer --- > System.Exception: Внутренний
--- Конец внутренней проверки стека исключений ---
в WindowsFormsApplication1.Form1.button1_Click (отправитель объекта, EventArgs e) в C:\svn\trunk\Code Base\Source.NET\WindowsFormsApplication1\Form1.cs: строка 55
в System.Windows.Forms.Control.OnClick(EventArgs e)
в System.Windows.Forms.Button.OnClick(EventArgs e)
в System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
в System.Windows.Forms.Control.WmMouseUp(Message & m, кнопка MouseButtons, клики Int32)
в System.Windows.Forms.Control.WndProc(Message & m)
в System.Windows.Forms.ButtonBase.WndProc(Message & m)
в System.Windows.Forms.Button.WndProc(Message & m)
в System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message & m)
в System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message & m)
в System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
Но если throw outer;
находится внутри вызова BeginInvoke
, как в приведенном выше коде, тогда обработчик ThreadException
получает только внутреннее исключение. Внешнее исключение удаляется до вызова ThreadException
, и все, что я получаю, это:
System.Exception: Внутренний
(Здесь нет стека вызовов, потому что inner
никогда не попадался. В более реалистичном примере, когда я поймал одно исключение и завернул его в повторный бросок, будет стек вызовов.)
То же самое происходит, если я использую SynchronizationContext.Current.Post
вместо BeginInvoke
: внешнее исключение удаляется, а обработчик ThreadException
получает только внутреннее исключение.
Я попробовал обернуть больше слоев исключений извне, в случае, если он просто отменил самое внешнее исключение, но это не помогло: по-видимому, где-то там цикл делал что-то вдоль строк while (e.InnerException != null) e = e.InnerException;
.
Я использую BeginInvoke
, потому что у меня есть код, который должен вызывать необработанное исключение, которое немедленно обрабатывается ThreadException
, но этот код находится внутри блока catch
выше стека вызовов (в частности, он внутри действия для a Task
, а Task
поймает исключение и прекратит его распространение). Я пытаюсь использовать BeginInvoke
для задержки throw
, пока в следующий раз сообщения не будут обработаны в цикле сообщений, когда я больше не буду внутри catch
. Я не привязан к конкретному решению BeginInvoke
; Я просто хочу бросить необработанное исключение.
Как я могу заставить исключение, включая его внутреннее исключение, достичь ThreadException
, даже если я внутри кого-то еще catch
-all?
(Я не могу вызвать метод ThreadException
-handler напрямую из-за зависимостей сборки: обработчик подключен кодом запуска EXE, тогда как моя текущая проблема находится в DLL более низкого уровня.)
Ответы
Ответ 1
Один из способов сделать это - поместить ссылку на внутреннее исключение в пользовательское свойство или Data
словарь, то есть оставить InnerException
свойство null и нести ссылку другим способом.
Конечно, для этого требуется установить какое-то соглашение, которое можно разделить между метательным кодом и кодом обработки. Лучшим было бы, вероятно, определить настраиваемый класс исключений с настраиваемым свойством в проекте, на который ссылаются обе части кода.
Пример кода (хотя ему нужно больше комментариев, чтобы объяснить, почему он делает сумасшедшие вещи, которые он делает):
public class ExceptionDecorator : Exception {
public ExceptionDecorator(Exception exception) : base(exception.Message) {
Exception = exception;
}
public Exception Exception { get; private set; }
}
// To throw an unhandled exception without losing its InnerException:
BeginInvoke(new Action(() => { throw new ExceptionDecorator(outer); }));
// In the ThreadException handler:
private void OnUnhandledException(object sender, ThreadExceptionEventArgs e) {
var exception = e.Exception;
if (exception is ExceptionDecorator)
exception = ((ExceptionDecorator) exception).Exception;
// ...
}
Ответ 2
Я предполагаю, что вы видите это поведение в системе x64 Windows, и это - довольно неизвестная деталь реализации x64 Windows. Прочитайте на нем здесь
В статье подробно рассказывается, как решить эту проблему, применив некоторое исправление, которое предположительно было отправлено с помощью Win7 SP1, но я столкнулся с этой проблемой несколько недель назад на Win7 SP1.
Кроме того, вы можете прикрепить событие AppDomain.FirstChanceException, которое дает вам доступ к каждому исключению, прежде чем оно будет передано в CLR для обработки
Ответ 3
Рекомендуемый способ распространения Исключения на более высокий уровень (помимо неявного перебора по ожиданию задачи) заключается в том, чтобы удалить catch-all из тела Task и вместо этого зарегистрировать продолжение Fault в Задаче, используя Task.ContinueWith, указав TaskContinuationOptions.OnlyOnFaulted
. Если вы работаете через промежуточный уровень и не имеете доступа к задаче, вы можете дополнительно обернуть это в свои собственные события UnhandledException, чтобы передать объект Exception вверх.