保留异常信息以便继续处理Task<T>。

10
我有一个任务<T>:
Task<A> someTask = ...

这个任务可能会成功、失败或取消。

当任务成功时,我想要改变结果,否则保留原结果。

someTask 抛出异常时,这似乎非常困难。

我尝试过:

Task<B> resultTask = StartMyTask().ContinueWith<B>(
    t => Foo(t.Result),
    TaskContinuationOptions.OnlyOnRanToCompletion);

这将导致someTask出现故障时,resultTask被取消。我希望它出现故障。
Task<B> resultTask = StartMyTask().ContinueWith<B>(
    t => Foo(t.Result));

这会因为.Result抛出异常而中断Visual Studio调试器。如果我按F5,resultTask会像预期的那样失败,但是这很糟糕。

是否有办法让resultTasksomeTask失败时具有与someTask相同的结果?


本质上,我想用任务来表达类似于这样的东西:

int F()
{
    throw new SomeException();
}

string G(int x)
{
    return x.ToString();
}

try
{
    string result = G(F());
}
catch (SomeException e)
{
    ...
}

你的问题是否可以理解为:如何在继续操作中跳过将原始异常包装成AggregateException?我认为这是不可能的,也是有意设计的。你必须习惯于在TPL中处理AggregateException,它无处不在。实际上,AggregateException有许多辅助方法,还可以参考这篇文章 - http://msdn.microsoft.com/en-us/magazine/ee321571.aspx 和这个 http://msdn.microsoft.com/en-us/library/dd997415.aspx。 - Shrike
@Shrike:真正困扰我的不是嵌套的AggregateException。我已经稍微改了一下问题的措辞。 - dtb
relatd?https://dev59.com/UV_Va4cB1Zd3GeqPRkFz - Ruben Bartelink
5个回答

5

我猜想原始异常将在第一个AggregateException中,如果你明白我的意思-你只需要解开两次,或者在外部的AggregateException上调用AggregateException.Flatten()


没错,使用Flatten方法可以解决问题。但是,由于新的AggregateException,我仍然感到非常困扰。我真的需要在调试器中禁用AggregateException吗?还是有其他的方法? - dtb
嗯,你可以为出错的任务编写一个继续测试,并执行解包操作。如果你经常需要这样做,可以使用扩展方法来完成,该方法接受另一个动作并进行封装。 - Jon Skeet
@dtb:但它抛出的异常不是在t.Result处,对吧? - Jon Skeet
@dtb:嗯,是的。在那种情况下,你期望/想要它做什么并不清楚。如果你指定了OnlyRanToCompletion,那么继续执行将被取消,否则不会。这正是你告诉它要做的! - Jon Skeet
如果我评估表达式 G(F()) 并且 F 抛出异常,那么表达式 G(F()) 的结果就是该异常,而不是 "G 被取消了,因为 F 抛出了异常"。我希望任务的行为也是如此。 - dtb
显示剩余4条评论

5

这似乎是有效的,与 @Stephen Cleary 建议的 "PipelineTransform" 可能相似。

Task<B> resultTask = StartMyTask().ContinueWith<Task<B>>(task =>
{
    var tcs = new TaskCompletionSource<B>();

    switch (task.Status)
    {
    case TaskStatus.Canceled:
        tcs.SetCanceled();
        break;

    case TaskStatus.Faulted:
        tcs.SetException(task.Exception);
        break;

    case TaskStatus.RanToCompletion:
        tcs.SetResult(Foo(task.Result));
        break;
    }

    return tcs.Task;
}).Unwrap();

1
是的,这基本上就是我想的 - 只需创建一个扩展方法,将 Foo 作为 Func<TInput, TOutput> 参数传递。 - Stephen Cleary

4
任务继续是独立的。它们可以用于构建所需的内容,但并不是专门为该场景设计的。
首先要问的问题是:这种关系是否可以视为父/子关系(例如,Foo 将是 StartMyTask 的父项)?如果有意义,那么您可能可以利用从子项到父项的状态传播。
如果将 Foo 视为“父项”,将 StartMyTask 视为“子项”在设计上行不通,则还有其他几个选项。对于您需要的内容,连续性有点低级(请记住,它们只是“在完成另一个任务时运行此任务”)。
听起来您可能正在尝试做更像“管道”的事情。目前,Rx 更适合此类事情。
基于任务的管道还没有真正出现。 ParallelExtensionsExtras 库 有一个任务管道类,而 Async CTP 有一个 TPL Dataflow 库,但这两者都未得到充分开发(例如,Pipeline 坚持在单独的线程中运行管道的每个阶段,而 Dataflow 没有自动传播异常甚至完成的机制)。
因此,如果您无法使用 Rx,则应编写自己的“PipelineTransform”扩展方法以用于任务,并使用显式 TCS 正确处理所有三种完成情况。

1
Rx确实是我的出发点,我深感沮丧的是任务远不如IObservables可组合。不幸的是,我不能在这个项目中使用Rx。Async CTP可以优雅地解决问题async Task<B> DoIt() { return Foo(await StartSomeTask()); }但我也不能使用它。你能展示一下PipelineTransform方法会是什么样子吗? - dtb

2
为了使Task<B>完全保留原始任务的异常状态,我们可以将dtb答案中的switch case更改为
    case TaskStatus.Faulted:
        tcs.SetException(task.Exception.InnerExceptions);
        break;

(请注意,这里是“InnerExceptions”,有一个's'。)
这样可以避免嵌套的AggregateException问题。

0

当您检查任务的结果时,您总是会得到AggregateException异常。如果您的错误处理代码可以与主要代码分开,则可以使用一种AOP方法,例如PostSharp:

[ErrorHandling]
public void doWord()
{
    string result = G(F());
}

其中ErrorHandling是:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
[MulticastAttributeUsage(
    MulticastTargets.Method,
    Inheritance = MulticastInheritance.Multicast,
    AllowMultiple = false)]
     public sealed class ErrorHandlingAttribute : OnMethodBoundaryAspect
     {
         public override void OnException(MethodExecutionArgs args)
         {
             base.OnException(args);
             Exception ex = args.Exception;
             AggregateException ae;
             if ((ae = ex as AggregateException) !=null)
                 ex = ae.InnerExceptions[0];

             // your error handling logic
         }
     }

我知道这可能看起来有些过度,但这只是一个想法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接