如何捕获从延续抛出的未处理异常?
How to catch an unhandled exception thrown from a continuation?
我无法捕获从延续任务中抛出的未处理异常。
为了演示这个问题,让我向您展示一些有效的代码。此代码来自基本 Windows Forms 应用程序。
首先,program.cs:
using System;
using System.Windows.Forms;
namespace WindowsFormsApplication3
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
Application.ThreadException += (sender, args) =>
{
MessageBox.Show(args.Exception.Message, "ThreadException");
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
};
try
{
Application.Run(new Form1());
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Application.Run() exception");
}
}
}
}
这会订阅所有可用的异常处理程序。 (实际上只提出了 Application.ThreadException
,但我想确保排除所有其他可能性。)
下面是导致未处理异常正确显示消息的表单:
using System;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApplication3
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
doWork();
this.Close();
}
void doWork()
{
Thread.Sleep(1000); // Simulate work.
throw new InvalidOperationException("TEST");
}
}
}
当您运行执行此操作时,一秒钟后会出现一个消息框,显示异常消息。
如您所见,我正在编写一个 "please wait" 样式的表单,它会执行一些后台工作,然后在工作完成后自动关闭。
据此,我在OnShown()
中添加了一个后台任务如下:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
Task.Factory.StartNew(doWork).ContinueWith
(
antecedent =>
{
if (antecedent.Exception != null)
throw antecedent.Exception;
this.Close();
},
TaskScheduler.FromCurrentSynchronizationContext()
);
}
我认为延续将在表单的上下文中 运行,异常会以某种方式被我在 program.cs
中订阅的未处理异常事件之一捕获。
不幸的是,没有发现任何问题,表格保持打开状态,没有任何迹象表明有任何问题。
有谁知道我应该怎么做,这样如果在工作任务中抛出异常并且没有明确捕获和处理,它将被外部未处理的异常事件捕获?
[编辑]
Niyoko Yuliawan 建议使用 await
。不幸的是我不能使用那个,因为这个项目被困在使用古老的.Net 4.0。但是,我可以确认使用 await
可以解决问题![=21=]
为了完整起见,如果我使用 .Net 4.5 或更高版本,这里是我可以使用的更简单和更易读的解决方案:
protected override async void OnShown(EventArgs e)
{
base.OnShown(e);
await Task.Factory.StartNew(doWork);
this.Close();
}
[EDIT2]
raidensan 还提出了一个看起来很有用的答案,但不幸的是,这也不起作用。我认为这只是稍微解决了问题。以下代码也无法导致显示异常消息 - 即使 运行 在调试器下使用它并在行 antecedent => { throw antecedent.Exception; }
上设置断点表明正在到达该行。
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
var task = Task.Factory.StartNew(doWork);
task.ContinueWith
(
antecedent => { this.Close(); },
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext()
);
task.ContinueWith
(
antecedent => { throw antecedent.Exception; },
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext()
);
}
你检查过TaskScheduler.UnobservedTaskException
事件here了吗?
有了这个,您可以在垃圾收集发生后捕获工作线程中发生的未观察到的异常。
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
Application.ThreadException += (sender, args) =>
{
MessageBox.Show(args.Exception.Message, "ThreadException");
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
};
try
{
Application.Run(new Form1());
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Application.Run() exception");
}
}
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
throw e.Exception;
}
这是任务:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
Task.Factory.StartNew(doWork);
Thread.Sleep(2000);
GC.Collect();
GC.WaitForPendingFinalizers();
}
void doWork()
{
Thread.Sleep(1000); // Simulate work.
throw new InvalidOperationException("TEST");
}
更新
您提到您使用的是.Net 4.0。使用 async/await
功能怎么样,它不会阻止 UI。您只需要从 nuget 添加 Microsoft Async
到您的项目。
现在修改OnShown如下(我添加了doWork
的代码,这样可以更明显):
protected async override void OnShown(EventArgs e)
{
base.OnShown(e);
await Task.Factory.StartNew(() =>
{
Thread.Sleep(1000); // Simulate work.
throw new InvalidOperationException("TEST");
})
.ContinueWith
(
antecedent => { this.Close(); },
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext()
)
.ContinueWith
(
antecedent => {
throw antecedent.Exception;
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext()
);
}
旧答案
找到解决方案,添加task.Wait();
。不确定它为什么有效:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
var task = Task.Factory.StartNew(doWork)
.ContinueWith
(
antecedent => { this.Close(); },
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext()
)
.ContinueWith
(
antecedent => {
throw antecedent.Exception;
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext()
);
// this is where magic happens.
task.Wait();
}
我找到了一个解决方法,我将 post 作为答案(但我不会将其标记为至少一天的答案,以防有人在此期间提出更好的答案!)
我决定走老路,使用 BeginInvoke()
而不是延续。然后它似乎按预期工作:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
Task.Factory.StartNew(() =>
{
try
{
doWork();
}
catch (Exception exception)
{
this.BeginInvoke(new Action(() => { throw new InvalidOperationException("Exception in doWork()", exception); }));
}
finally
{
this.BeginInvoke(new Action(this.Close));
}
});
}
但是,至少我现在知道为什么我的原始代码不起作用了。原因见evk的回答!
即使您在 UI 线程上抛出异常,它 而不是 意味着它最终会到达 Application.ThreadException
。在这种情况下,它被Task
exception handler(设置了Task.Exception
、Task.IsFaulted
等的handler)拦截了,实际上变成了unobserved task exception。把它想象成 Task
使用 Control.Invok
e 来执行你的延续,而 Control.Invoke
是同步的,这意味着它的异常不能传递给 Application.ThreadException
,因为 winforms 不知道它是否将由调用者处理(这与 Control.BeginInvoke
不同,后者的异常将始终传递给 Application.ThreadException
)。
您可以通过订阅 TaskScheduler.UnobservedTaskException
和 在您的继续完成后的某个时间强制垃圾回收来验证。
长话短说 - 在这种情况下,您的延续是否在 UI 线程上运行是无关紧要的 - 您的延续中所有未处理的异常将以 UnobservedTaskException 而不是 ThreadException 结束。在这种情况下手动调用异常处理程序而不依赖那些 "last resort" 处理程序是合理的。
我无法捕获从延续任务中抛出的未处理异常。
为了演示这个问题,让我向您展示一些有效的代码。此代码来自基本 Windows Forms 应用程序。
首先,program.cs:
using System;
using System.Windows.Forms;
namespace WindowsFormsApplication3
{
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
Application.ThreadException += (sender, args) =>
{
MessageBox.Show(args.Exception.Message, "ThreadException");
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
};
try
{
Application.Run(new Form1());
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Application.Run() exception");
}
}
}
}
这会订阅所有可用的异常处理程序。 (实际上只提出了 Application.ThreadException
,但我想确保排除所有其他可能性。)
下面是导致未处理异常正确显示消息的表单:
using System;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApplication3
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
doWork();
this.Close();
}
void doWork()
{
Thread.Sleep(1000); // Simulate work.
throw new InvalidOperationException("TEST");
}
}
}
当您运行执行此操作时,一秒钟后会出现一个消息框,显示异常消息。
如您所见,我正在编写一个 "please wait" 样式的表单,它会执行一些后台工作,然后在工作完成后自动关闭。
据此,我在OnShown()
中添加了一个后台任务如下:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
Task.Factory.StartNew(doWork).ContinueWith
(
antecedent =>
{
if (antecedent.Exception != null)
throw antecedent.Exception;
this.Close();
},
TaskScheduler.FromCurrentSynchronizationContext()
);
}
我认为延续将在表单的上下文中 运行,异常会以某种方式被我在 program.cs
中订阅的未处理异常事件之一捕获。
不幸的是,没有发现任何问题,表格保持打开状态,没有任何迹象表明有任何问题。
有谁知道我应该怎么做,这样如果在工作任务中抛出异常并且没有明确捕获和处理,它将被外部未处理的异常事件捕获?
[编辑]
Niyoko Yuliawan 建议使用 await
。不幸的是我不能使用那个,因为这个项目被困在使用古老的.Net 4.0。但是,我可以确认使用 await
可以解决问题![=21=]
为了完整起见,如果我使用 .Net 4.5 或更高版本,这里是我可以使用的更简单和更易读的解决方案:
protected override async void OnShown(EventArgs e)
{
base.OnShown(e);
await Task.Factory.StartNew(doWork);
this.Close();
}
[EDIT2]
raidensan 还提出了一个看起来很有用的答案,但不幸的是,这也不起作用。我认为这只是稍微解决了问题。以下代码也无法导致显示异常消息 - 即使 运行 在调试器下使用它并在行 antecedent => { throw antecedent.Exception; }
上设置断点表明正在到达该行。
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
var task = Task.Factory.StartNew(doWork);
task.ContinueWith
(
antecedent => { this.Close(); },
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext()
);
task.ContinueWith
(
antecedent => { throw antecedent.Exception; },
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext()
);
}
你检查过TaskScheduler.UnobservedTaskException
事件here了吗?
有了这个,您可以在垃圾收集发生后捕获工作线程中发生的未观察到的异常。
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
Application.ThreadException += (sender, args) =>
{
MessageBox.Show(args.Exception.Message, "ThreadException");
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
};
try
{
Application.Run(new Form1());
}
catch (Exception exception)
{
MessageBox.Show(exception.Message, "Application.Run() exception");
}
}
private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
throw e.Exception;
}
这是任务:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
Task.Factory.StartNew(doWork);
Thread.Sleep(2000);
GC.Collect();
GC.WaitForPendingFinalizers();
}
void doWork()
{
Thread.Sleep(1000); // Simulate work.
throw new InvalidOperationException("TEST");
}
更新
您提到您使用的是.Net 4.0。使用 async/await
功能怎么样,它不会阻止 UI。您只需要从 nuget 添加 Microsoft Async
到您的项目。
现在修改OnShown如下(我添加了doWork
的代码,这样可以更明显):
protected async override void OnShown(EventArgs e)
{
base.OnShown(e);
await Task.Factory.StartNew(() =>
{
Thread.Sleep(1000); // Simulate work.
throw new InvalidOperationException("TEST");
})
.ContinueWith
(
antecedent => { this.Close(); },
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext()
)
.ContinueWith
(
antecedent => {
throw antecedent.Exception;
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext()
);
}
旧答案
找到解决方案,添加task.Wait();
。不确定它为什么有效:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
var task = Task.Factory.StartNew(doWork)
.ContinueWith
(
antecedent => { this.Close(); },
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.FromCurrentSynchronizationContext()
)
.ContinueWith
(
antecedent => {
throw antecedent.Exception;
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext()
);
// this is where magic happens.
task.Wait();
}
我找到了一个解决方法,我将 post 作为答案(但我不会将其标记为至少一天的答案,以防有人在此期间提出更好的答案!)
我决定走老路,使用 BeginInvoke()
而不是延续。然后它似乎按预期工作:
protected override void OnShown(EventArgs e)
{
base.OnShown(e);
Task.Factory.StartNew(() =>
{
try
{
doWork();
}
catch (Exception exception)
{
this.BeginInvoke(new Action(() => { throw new InvalidOperationException("Exception in doWork()", exception); }));
}
finally
{
this.BeginInvoke(new Action(this.Close));
}
});
}
但是,至少我现在知道为什么我的原始代码不起作用了。原因见evk的回答!
即使您在 UI 线程上抛出异常,它 而不是 意味着它最终会到达 Application.ThreadException
。在这种情况下,它被Task
exception handler(设置了Task.Exception
、Task.IsFaulted
等的handler)拦截了,实际上变成了unobserved task exception。把它想象成 Task
使用 Control.Invok
e 来执行你的延续,而 Control.Invoke
是同步的,这意味着它的异常不能传递给 Application.ThreadException
,因为 winforms 不知道它是否将由调用者处理(这与 Control.BeginInvoke
不同,后者的异常将始终传递给 Application.ThreadException
)。
您可以通过订阅 TaskScheduler.UnobservedTaskException
和 在您的继续完成后的某个时间强制垃圾回收来验证。
长话短说 - 在这种情况下,您的延续是否在 UI 线程上运行是无关紧要的 - 您的延续中所有未处理的异常将以 UnobservedTaskException 而不是 ThreadException 结束。在这种情况下手动调用异常处理程序而不依赖那些 "last resort" 处理程序是合理的。