数据块中的异常处理

Posted

技术标签:

【中文标题】数据块中的异常处理【英文标题】:Exception Handling In DataBlocks 【发布时间】:2021-01-28 20:30:50 【问题描述】:

我正在尝试了解 TPL 中的异常处理。

以下代码似乎吞下了异常:

var processor = new ActionBlock<int>((id) => SomeAction(id), new ExecutionDataflowBlockOptions  ... );


async Task SomeAction(int merchantID)

    //Exception producing code
    ...

并且收听TaskScheduler.UnobservedTaskException 事件也不会收到任何东西。

那么,这是否意味着动作块在运行动作时会自己进行 try-catch?

在某处有这方面的官方文档吗?

【问题讨论】:

【参考方案1】:

更新

DataFlow 块的异常处理行为在Exception Handling in TPL DataFlow Networks中解释

**原创

此代码不会吞下异常。如果您使用await processor.Completion 等待该块完成,您将收到异常。如果您在调用Complete() 之前使用循环将消息发送到块,您也需要一种方法来停止循环。一种方法是使用CancellationTokenSource 并在出现异常时发出信号:

void SomeAction(int i,CancellationTokenSource cts)

    try
    
        ...
    
    catch(Exception exc)
    
        //Log the error then
        cts.Cancel();
       //Optionally throw
    

发帖代码不用改那么多,只需要检查是否

var cts=new CancellationTokenSource();
var token=cts.Token;
var dopOptions=new new ExecutionDataflowBlockOptions  
                           MaxDegreeOfParallelism=10,
                           CancellationToken=token
;
var block= new ActioBlock<int>(i=>SomeAction(i,cts),dopOptions);

while(!token.IsCancellationRequested && someCondition)

    block.Post(...);

block.Complete();
await block.Completion;

当动作抛出时,令牌发出信号并且块结束。如果异常被操作重新抛出,它也会被await block.Completion 重新抛出。

如果这看起来令人费解,那是因为这在某种程度上是块的边缘情况。 DataFlow 用于创建管道或块网络。

一般情况

Dataflow 这个名字很重要。 您不必使用相互调用的方法来构建程序,而是使用相互传递消息的处理块。没有接收结果和异常的父方法。块的管道无限期地保持活动状态以接收和处理消息,直到某个外部控制器告诉它停止,例如通过在头块上调用 Complete 或发出信号传递给每个块的 CancellationToken。

块不应允许发生未处理的异常,即使它是独立的 ActionBlock。如您所见,除非您已经调用了Complete()await Completion,否则您不会得到异常。

当块内部发生未处理的异常时,块进入故障状态。该状态传播到与PropagateCompletion 选项链接的所有下游块。 上游块不受影响,这意味着它们可以继续工作,将消息存储在其输出缓冲区中,直到进程耗尽内存,或者因为没有收到来自块的响应而死锁。

正确的故障处理

块应该根据应用程序的逻辑捕获异常并决定做什么:

    记录并继续处理。这与 Web 应用程序的工作方式没有什么不同 - 请求期间的异常不会导致服务器停机。 明确地向另一个块发送错误消息。这可行,但这种类型的硬编码不是很适合数据流。 使用带有某种错误指示器的消息类型。也许是一个 Success 标志,也许是一个包含消息或错误的 Envelope&lt;TMessage&gt; 对象。 优雅地取消整个流水线,通过发信号通知所有块取消,通过发信号通知CancellationTokenSource 用于产生所有块使用的CancellationTokens。这相当于普通程序中的throw

#3 是最通用的选项。下游块可以检查信封并忽略或传播失败的消息而不进行处理。本质上,失败的消息会绕过下游块。

另一种选择是使用LinkTo 中的predicate 参数并将失败的消息发送到记录器块,并将成功的消息发送到下一个下游块。在复杂的场景中,这可以用于例如重试某些操作并将结果发送到下游。

这些概念和图像来自 Scott Wlaschin 的 Railway Oriented Programming

【讨论】:

【参考方案2】:

TaskScheduler.UnobservedTaskException 事件不是处理故障任务异常的可靠/确定性方法,因为它会延迟到垃圾收集器清理故障任务为止。这可能在错误发生很久之后才发生。

数据流块吞噬的唯一异常类型是OperationCanceledException(AFAIK 出于未记录的原因)。所有其他异常都会导致块转换到故障状态。故障块的Completion 属性(Task)也发生故障(processor.Completion.IsFaulted == true)。您可以将延续附加到 Completion 属性,以便在块失败时接收通知。例如,您可以通过简单地使进程崩溃来确保异常不会被忽视:

processor.Completion.ContinueWith(t =>

    ThreadPool.QueueUserWorkItem(_ => throw t.Exception);
, default, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);

这是可行的,因为在 ThreadPool 上引发未处理的异常会导致应用程序终止(在引发 AppDomain.CurrentDomain.UnhandledException 事件之后)。

如果您的应用程序有 GUI(WinForms/WPF 等),那么您可以在 UI 线程上抛出异常,从而允许更优雅的错误处理:

var uiContext = SynchronizationContext.Current;
processor.Completion.ContinueWith(t =>

    uiContext.Post(_ => throw t.Exception, null);
, default, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);

这将在 WinForms 中引发 Application.ThreadException 事件。

【讨论】:

以上是关于数据块中的异常处理的主要内容,如果未能解决你的问题,请参考以下文章

如何使用多个catch块处理异常

异常处理(动手动脑)

动手动脑-异常处理

Java中的异常处理try catch(第八周课堂示例总结)

在线程中处理在 catch 块中抛出的异常的最佳实践。 (。网)

从完成处理程序/块中引发自定义异常会使目标 c 中的应用程序崩溃