我有一个C#Extension方法,可以与任务一起使用,以确保抛出的任何异常都被观察到最低限度,以免崩溃托管进程。在.NET4.5中,行为略有改变,因此不会发生这种情况,但仍会触发未观察到的异常事件。我在这里的挑战是编写一个测试来证明扩展方法是有效的。我正在使用NUnit Test Framework,ReSharper是测试运行者。
我试过了:
var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(() =>
{
throw new NaiveTimeoutException();
return new DateTime?();
});
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
Assert.IsTrue
上的测试始终失败。当我手动运行此测试时,在LINQPad之类的操作中,我得到wasUnobservedException
的预期行为,即true
。
我猜测测试框架正在捕获异常并观察它,以致TaskScheduler.UnobservedTaskException
永远不会被触发。
我尝试按如下方式修改代码:
var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = TaskEx.Run(async () =>
{
await TaskEx.Delay(5000).WithTimeout(1000).Wait();
return new DateTime?();
});
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
我在此代码中进行的尝试是在抛出异常之前使任务获得GC'd,以便终结器将看到未捕获的,未观察到的异常。然而,这导致了上述相同的失败。
实际上,测试框架是否存在某种异常处理程序?如果是这样,有办法吗?或者我只是把事情搞砸了,有更好/更容易/更清洁的方法吗?
答案 0 :(得分:6)
我发现这种方法存在一些问题。
首先,有一个明确的竞争条件。当TaskEx.Run
返回任务时,它只是将请求排队到线程池;任务尚未完成。
其次,你遇到了一些垃圾收集器细节。在调试中编译时 - 实际上,无论何时编译器都感觉如此 - 局部变量的生命周期(即res
)都会扩展到方法的末尾。
考虑到这两个问题,我能够通过以下代码:
var wasUnobservedException = false;
TaskScheduler.UnobservedTaskException += (s, args) => wasUnobservedException = true;
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(wasUnobservedException);
然而,仍有两个问题:
仍有(技术上)竞争条件。尽管由于任务终结器而引发UnobservedTaskException
,但无法保证AFAIK从任务终结器中引发。目前,它似乎是,但在我看来,这似乎是一个非常不稳定的解决方案(考虑到限制终结器假设)。因此,在框架的未来版本中,我不会太惊讶于终结器只是将UnobservedTaskException
排队到线程池而不是直接执行它。在这种情况下,您不能再依赖于事件在任务完成时处理的事实(上述代码隐含的假设)。
在单元测试中修改全局状态(UnobservedTaskException
)也存在问题。
考虑到这两个问题,我最终得到:
var mre = new ManualResetEvent(initialState: false);
EventHandler<UnobservedTaskExceptionEventArgs> subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
res = null; // Allow the task to be GC'ed
GC.Collect();
GC.WaitForPendingFinalizers();
if (!mre.WaitOne(10000))
Assert.Fail();
}
finally
{
TaskScheduler.UnobservedTaskException -= subscription;
}
考虑到它的复杂性,它也通过但是值得怀疑。
答案 1 :(得分:2)
只需在@Stephen Cleary的解决方案中添加一个补语(请不要支持我的回答)。
如前所述,在“调试”模式下编译时,局部变量的生存期会延长到方法的末尾,因此,所提出的解决方案仅在以“发布”模式下编译代码时有效(不附带调试器)
如果您确实需要对该行为进行单元测试(或使其在“调试”模式下工作),则可以“欺骗” GC,将启动Task的代码放入Action(或本地函数)中。这样做之后,在调用操作之后,该任务将可供GC收集。
var mre = new ManualResetEvent(initialState: false);
EventHandler<UnobservedTaskExceptionEventArgs> subscription = (s, args) => mre.Set();
TaskScheduler.UnobservedTaskException += subscription;
try
{
Action runTask = () =>
{
var res = Task.Run(() =>
{
throw new NotImplementedException();
return new DateTime?();
});
((IAsyncResult)res).AsyncWaitHandle.WaitOne(); // Wait for the task to complete
};
runTask.Invoke();
GC.Collect();
GC.WaitForPendingFinalizers();
if (!mre.WaitOne(10000))
Assert.Fail();
}
finally
{
TaskScheduler.UnobservedTaskException -= subscription;
}