处理任务异常——自定义TaskScheduler
Handling Task Exceptions - custom TaskScheduler
我正在编写一个自定义的 FIFO 队列线程限制任务调度程序。
我希望能够保证未处理任务异常立即冒泡或升级,并继续结束进程。
我还希望能够 Task.Wait()
和 处理 一个可能弹出的 AggregateException
,而不会导致进程到此结束。
这两个要求可以同时满足吗?我是不是想错了?
更多信息
问题的症结在于我并不总是想Task.Wait()
完成一项任务。
在我的自定义任务调度程序上执行任务后,我知道未处理的任务异常最终会 escalate when the Task is cleaned up by the GC。
If you do not wait on a task that propagates an exception, or access its Exception property, the exception is escalated according to the .NET exception policy when the task is garbage-collected.
但谁知道在应用程序执行的环境中什么时候会发生这种情况 - 这在设计时无法确定。
对我来说,这意味着任务中未处理的异常可能会永远未被发现。这让我很反感。
如果我想确保立即升级未处理的任务异常,我可以在任务执行后在我的任务调度程序中执行以下操作:
while ( taskOnQueue )
{
/// dequeue a task
TryExecuteTask(task);
if (task.IsFaulted)
{
throw task.Exception.Flatten();
}
}
但是通过这样做,我基本上可以保证异常将始终终止进程,无论如何通过在 Task.Wait()
(或 TaskScheduler.UnobservedException
处捕获 AggregateException
来处理它] 事件处理程序)。
鉴于这些选项 - 我真的必须选择其中之一吗?
Given these options - do I really have to choose one or the other?
是的,你几乎做到了。确定 Task
不会被 Wait
ed 的唯一方法是找出正在执行的程序无法访问 Task
,这就是 GC 所做的。即使您可以弄清楚某个线程当前是否正在 Wait
ing Task
,这还不够:Task
可能存储在某个数据结构中的某个地方,它将是 [=11] =]在未来的某个时候继续。
我能想到的唯一选择是在每个 Task
完成后调用 GC.Collect()
,这是一个非常糟糕的选择,尤其是从性能角度来看。这样,每个在完成时无法到达的 Task
将立即被视为未处理。但即使这样也不可靠,因为 Task
完成后可能无法访问。
To me, this means that unhandled exceptions in tasks can potentially go undetected - forever. That leaves a bad taste in my mouth.
这是真的,你无法改变它。第 (1) 点是失败的原因。
我不明白以下要点:
if (task.IsFaulted)
{
throw task.Exception.Flatten();
}
这意味着 任何 任务异常,即使是已处理的异常,也会抛出此异常。这限制了您可以在此调度程序上合理创建的任务。
(我也不明白你为什么要扁平化;甚至 task.GetAwaiter().GetResult()
更好。可能,你应该换行而不是假装重新抛出。)
另见 svicks 回答。
难道你不能只挂钩未处理的异常事件并将此类情况报告给开发人员吗?未处理的任务异常有时是错误,但根据我的经验,很少是致命的。
如果你真的不需要任务抛出异常,你可以使用包装任务主体的辅助方法来安排任务:
try {
body();
} catch (...) { ... }
这将是一种相当简洁的方式,灵活且不依赖于自定义调度程序。
发布一个答案,这样我就可以描述对 usr 评论中讨论的选项很重要的细节。
当使用单独的方法记录或处理任务异常时,(至少)有两种基于任务的方法:
- 使用
task.ContinueWith()
的延续任务
- 在第一个任务之后立即创建和安排的第二个任务
在我的例子中,这种区别很重要,因为自定义调度程序负责保证在保证数量的线程中呈现给它的任务的 FIFO 调度。
注意:由于自定义调度程序的运行,第一个任务完成后,第二个任务保证 运行 在与第一个相同的线程上。
选项 1 - 延续任务
Task task = new Task(() => DoStuff());
task.ContinueWith(t => HandleTaskExceptions(t), customScheduler);
task.Start(customScheduler);
选项 2 - 在第一个任务之后立即安排第二个任务
Task task = new Task(() => DoStuff());
task.Start(customScheduler);
Task task2 = new Task(() => HandleTaskExceptions(task));
task2.Start(customScheduler);
区别在于第二个任务何时被安排到运行。
使用选项 1 和 task continuation means that:
The returned Task will not be scheduled for execution until the current task has completed, whether it completes due to running to completion successfully, faulting due to an unhandled exception, or exiting out early due to being canceled.
这意味着可以将延续放在任务队列的末尾,这进一步意味着在完成其他工作之前可能不会处理或记录异常。这可能会影响整个应用程序状态。
使用选项 2,保证在第一个任务完成后立即处理异常,因为处理程序任务是队列中的下一个。
HandleTaskExceptions()
可以简单为:
void HandleTaskExceptions(Task task)
{
if (task.IsFaulted)
{
/// handle task exceptions
}
}
我正在编写一个自定义的 FIFO 队列线程限制任务调度程序。
我希望能够保证未处理任务异常立即冒泡或升级,并继续结束进程。
我还希望能够
Task.Wait()
和 处理 一个可能弹出的AggregateException
,而不会导致进程到此结束。
这两个要求可以同时满足吗?我是不是想错了?
更多信息
问题的症结在于我并不总是想Task.Wait()
完成一项任务。
在我的自定义任务调度程序上执行任务后,我知道未处理的任务异常最终会 escalate when the Task is cleaned up by the GC。
If you do not wait on a task that propagates an exception, or access its Exception property, the exception is escalated according to the .NET exception policy when the task is garbage-collected.
但谁知道在应用程序执行的环境中什么时候会发生这种情况 - 这在设计时无法确定。
对我来说,这意味着任务中未处理的异常可能会永远未被发现。这让我很反感。
如果我想确保立即升级未处理的任务异常,我可以在任务执行后在我的任务调度程序中执行以下操作:
while ( taskOnQueue )
{
/// dequeue a task
TryExecuteTask(task);
if (task.IsFaulted)
{
throw task.Exception.Flatten();
}
}
但是通过这样做,我基本上可以保证异常将始终终止进程,无论如何通过在 Task.Wait()
(或 TaskScheduler.UnobservedException
处捕获 AggregateException
来处理它] 事件处理程序)。
鉴于这些选项 - 我真的必须选择其中之一吗?
Given these options - do I really have to choose one or the other?
是的,你几乎做到了。确定 Task
不会被 Wait
ed 的唯一方法是找出正在执行的程序无法访问 Task
,这就是 GC 所做的。即使您可以弄清楚某个线程当前是否正在 Wait
ing Task
,这还不够:Task
可能存储在某个数据结构中的某个地方,它将是 [=11] =]在未来的某个时候继续。
我能想到的唯一选择是在每个 Task
完成后调用 GC.Collect()
,这是一个非常糟糕的选择,尤其是从性能角度来看。这样,每个在完成时无法到达的 Task
将立即被视为未处理。但即使这样也不可靠,因为 Task
完成后可能无法访问。
To me, this means that unhandled exceptions in tasks can potentially go undetected - forever. That leaves a bad taste in my mouth.
这是真的,你无法改变它。第 (1) 点是失败的原因。
我不明白以下要点:
if (task.IsFaulted)
{
throw task.Exception.Flatten();
}
这意味着 任何 任务异常,即使是已处理的异常,也会抛出此异常。这限制了您可以在此调度程序上合理创建的任务。
(我也不明白你为什么要扁平化;甚至 task.GetAwaiter().GetResult()
更好。可能,你应该换行而不是假装重新抛出。)
另见 svicks 回答。
难道你不能只挂钩未处理的异常事件并将此类情况报告给开发人员吗?未处理的任务异常有时是错误,但根据我的经验,很少是致命的。
如果你真的不需要任务抛出异常,你可以使用包装任务主体的辅助方法来安排任务:
try {
body();
} catch (...) { ... }
这将是一种相当简洁的方式,灵活且不依赖于自定义调度程序。
发布一个答案,这样我就可以描述对 usr 评论中讨论的选项很重要的细节。
当使用单独的方法记录或处理任务异常时,(至少)有两种基于任务的方法:
- 使用
task.ContinueWith()
的延续任务
- 在第一个任务之后立即创建和安排的第二个任务
在我的例子中,这种区别很重要,因为自定义调度程序负责保证在保证数量的线程中呈现给它的任务的 FIFO 调度。
注意:由于自定义调度程序的运行,第一个任务完成后,第二个任务保证 运行 在与第一个相同的线程上。
选项 1 - 延续任务
Task task = new Task(() => DoStuff());
task.ContinueWith(t => HandleTaskExceptions(t), customScheduler);
task.Start(customScheduler);
选项 2 - 在第一个任务之后立即安排第二个任务
Task task = new Task(() => DoStuff());
task.Start(customScheduler);
Task task2 = new Task(() => HandleTaskExceptions(task));
task2.Start(customScheduler);
区别在于第二个任务何时被安排到运行。
使用选项 1 和 task continuation means that:
The returned Task will not be scheduled for execution until the current task has completed, whether it completes due to running to completion successfully, faulting due to an unhandled exception, or exiting out early due to being canceled.
这意味着可以将延续放在任务队列的末尾,这进一步意味着在完成其他工作之前可能不会处理或记录异常。这可能会影响整个应用程序状态。
使用选项 2,保证在第一个任务完成后立即处理异常,因为处理程序任务是队列中的下一个。
HandleTaskExceptions()
可以简单为:
void HandleTaskExceptions(Task task)
{
if (task.IsFaulted)
{
/// handle task exceptions
}
}