处理任务异常 - 自定义 TaskScheduler

Posted

技术标签:

【中文标题】处理任务异常 - 自定义 TaskScheduler【英文标题】:Handling Task Exceptions - custom TaskScheduler 【发布时间】:2016-06-27 10:28:00 【问题描述】:

我正在编写一个自定义的 FIFO 队列线程限制任务调度程序。

    我希望能够保证未处理任务异常立即冒泡或升级,并继续结束流程。

    我还希望能够Task.Wait()处理可能会弹出的AggregateException,而不会导致进程结束。

这两个要求可以同时满足吗?我是不是想错了?


更多信息

问题的症结在于我并不总是想Task.Wait() 完成一项任务。

在我的自定义任务调度器上执行任务后,我知道未处理的任务异常最终会escalate when the Task is cleaned up by the GC。

如果您不等待传播异常的任务或访问其 Exception 属性,则在对任务进行垃圾收集时,将根据 .NET 异常策略升级异常。

但谁知道在应用程序执行的环境中何时会发生这种情况 - 这无法在设计时确定。

对我来说,这意味着任务中未处理的异常可能-永远未被发现。这让我觉得很糟糕。

如果我想确保立即上报未处理的任务异常,我可以在任务执行后在我的任务调度程序中执行以下操作:

while ( taskOnQueue )

    /// dequeue a task

    TryExecuteTask(task);

    if (task.IsFaulted) 
     
        throw task.Exception.Flatten(); 
    

但通过这样做,我基本上保证异常将始终终止进程,无论它如何通过在 Task.Wait()(或在 TaskScheduler.UnobservedException 事件处理程序中)捕获 AggregateException 来处理。

鉴于这些选项 - 我真的必须选择其中一个吗?

【问题讨论】:

【参考方案1】:

对我来说,这意味着任务中未处理的异常可能永远未被检测到。这在我的嘴里留下了不好的味道。

这是真的,你无法改变它。第 (1) 点是失败的原因。

我不明白以下几点:

if (task.IsFaulted) 
 
    throw task.Exception.Flatten(); 

这意味着任何任务异常,即使是已处理的异常,都会抛出此异常。这限制了您可以在此调度程序上合理创建的任务。

(我也不明白你为什么会变平;即使task.GetAwaiter().GetResult() 更好。也许,你应该换行而不是假重投。)

另见 svicks 的回答。

你不能直接钩住未处理的异常事件,然后向开发人员报告这种情况吗?未处理的任务异常有时是错误,但根据我的经验很少是致命的。

如果你真的不需要任务来抛出异常,你可以使用一个包裹任务主体的辅助方法来调度任务:

try 
 body();
 catch (...)  ... 

这将是一种相当干净的方式,灵活且不依赖于自定义调度程序。

【讨论】:

谢谢。自定义调度程序不是为了处理任务异常,它需要比默认调度程序提供的更具确定性的任务执行。 if (task.IsFaulted) ... 代码的重点是立即 导致异常升级,而不是在没有Task.Wait() 等待任务的情况下等待GC。但是您注意到的警告是正确的,这正是我在这些选项之间陷入困境的原因。 我实际上认为将任务包装在 try-catch 中将是一个可接受的解决方案,无论是在任务代码本身中,还是通过使用辅助方法或仅用于等待完成的先例任务第一个。 对,我的意思是建议一个继续任务来观察和记录异常,但由于某种原因,它在写答案时从我的脑海中溜走了。 是的,我认为这是另一种好方法。我可以在那里观察并记录异常,或者如果我想根据异常的性质满足立即升级策略,我可以将异常直接重新抛出到线程池中。【参考方案2】:

鉴于这些选项 - 我真的必须选择其中一个吗?

是的,你几乎可以。确定Task 不会被Waited on 的唯一方法是找出正在执行的程序无法访问Task,这就是GC 所做的。即使您可以弄清楚Task 上的某个线程当前是否在Waiting,这还不够:Task 可能存储在某个数据结构中的某个地方,它会在某个时间点被Waited on未来。

我能想到的唯一选择,而且这是一个非常糟糕的选择,尤其是从性能角度来看,是在每个 Task 完成后调用 GC.Collect()。这样,在完成时无法访问的每个Task 将立即被视为未处理。但即使这样也不可靠,因为 Task 在完成后可能会变得无法访问。

【讨论】:

是的,我同意 - 我对干扰 GC 的正常操作一点也不感兴趣。【参考方案3】:

发布答案,以便我可以描述对带有 usr 的 cmets 中讨论的选项很重要的细节。

当使用单独的方法来记录或处理任务异常时,有(至少)两种基于任务的方法:

    使用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 一起使用:

返回的Task在当前任务完成之前不会被调度执行,无论是由于运行到完成而完成,由于未处理的异常导致故障,还是由于被取消而提前退出。

这意味着可以将延续放置在任务队列的末尾,这进一步意味着在完成其他工作之前可能不会处理或记录异常。这可能会影响整个应用程序状态。

使用选项 2,可以保证在第一个任务完成后立即处理异常,因为 handler-task 是队列中的下一个。

HandleTaskExceptions() 可以这么简单:

void HandleTaskExceptions(Task task)

    if (task.IsFaulted)
    
        /// handle task exceptions
    

【讨论】:

以上是关于处理任务异常 - 自定义 TaskScheduler的主要内容,如果未能解决你的问题,请参考以下文章

十一、SpringMVC之自定义异常处理器

pythonThreadpool线程池任务终止简单示例

springmvc在处理请求过程中出现异常信息交由异常处理器进行处理,自定义异常处理器可以实现一个系统的异常处理逻辑。为了区别不同的异常通常根据异常类型自定义异常类,这里我们创建一个自定义系统异常,如

Java 求大神们解答:自定义异常,处理异常

Java学习笔记3.10.5 异常处理 - 自定义异常

SpringCloud请求异常处理封装BusinessException自定义异常类