【问题标题】:Exception behaviour for Task depends on `async` keywordTask 的异常行为取决于 `async` 关键字
【发布时间】:2016-08-05 01:25:32
【问题描述】:

我有以下场景:

组件A提供接口IMyInterface和注册机制,供其他组件注册其实现者。

界面如下:

public interface IMyInterface 
{
    Task DoSomethingAsync(Context context);
}

在组件 A 中,我想为所有实现者“并行”启动方法,然后等待任务。 实现者可能会在他们的实现中抛出异常(在我的具体场景中,涉及到 IO,我希望不时发生 IOExceptions;组件 A 确保正确捕获异常......)。

代码如下所示

var tasks = new List<Task>();

foreach (var impl in implementors)
{
    var context = ...;
    tasks.Add(impl.DoSomethingAsync(context));   
}

// now do something different that takes some time

try
{
    await Task.WhenAll(tasks);
}
catch(Exception e)
{
    // swallow. we handle the exceptions for each task below.
}

foreach (var task in tasks)
{
    if (task.IsFaulted)
        // log, recover, etc...
}

所以这是我的问题: 由于我await 用于单个任务,因此异常行为取决于实现者“创建”返回的Task 的方式。

public class Implementor1 : IMyInterface
{
    public async Task DoSomethingAsync(Context context)
    {
        // no awaits used in code here! 
        throw new Exception("oh the humanity");
    }
}

public class Implementor2 : IMyInterface
{
    public Task DoSomethingAsync(Context context)
    {
        throw new Exception("oh the humanity");
        return Task.CompletedTask;
    }
}

(注意区别:第一个实现者使用了async 关键字。)

在实现者 1 上调用 DoSomethingAsync 时,组件 A 中没有引发异常。任务对象设置为“故障”,我可以从任务中检索异常。 -> 正如我所料。

在实现者 2 上调用 DoSomethingAsync 时,立即抛出异常。 -> 不是我想要的。

问题来了: 如何处理这种情况以始终观察行为 1?我无法控制其他组件的作者如何实现我的界面。

【问题讨论】:

    标签: c# asynchronous exception-handling


    【解决方案1】:

    如果你想返回一个出错的Task,而不是让方法本身抛出异常,你需要创建一个Task,将其标记为出错,然后返回。

    使用 .NET 4.6,您可以编写:

    return Task.FromException(new Exception("oh the humanity"));
    

    如果您使用的是早期版本的 .NET,则可以使用以下 FromException 实现:

    public static Task FromException(Exception e)
    {
        var tcs = new TaskCompletionSource<bool>();
        tcs.SetException(e);
        return tcs.Task;
    }
    public static Task<T> FromException<T>(Exception e)
    {
        var tcs = new TaskCompletionSource<T>();
        tcs.SetException(e);
        return tcs.Task;
    }
    

    【讨论】:

    • 是的,但正如我所说,我无法控制其他组件的作者如何实现我的界面。当他们从不使用await 时,编译器建议删除async 关键字。由于实现很可能会进行 IO:如果作者忘记将 IOException 包装在 Task 中怎么办?我如何编写健壮的代码来处理这种情况?
    • @cpt.jazz 您捕获异常并返回代表该异常的任务。显然你不能改变他们的方法,你改变你的(如果它已经返回了你想要的,你的方法根本不需要做任何事情)。
    • 所以你的意思是这样的: foreach (var impl in implementors) { var context = ...;尝试 { tasks.Add(impl.DoSomethingAsync(context)); } catch(Exception e) { tasks.Add(Task.FromException(e)); } }
    • @cpt.jazz 当然,如果你想这样做的话。
    【解决方案2】:

    AFAIU,您无法修改接口的实现,但您希望拥有一致的异常处理策略。

    好吧,您可以将调用包装在另一个异步方法中。

    private async Task DoSomethingAsync(IMyInterface instance, Context context)
    {
        await instance.DoSomethingAsync(context);
    }
    

    并将其称为

    foreach (var impl in implementors)
    {
        var context = ...;
        tasks.Add(DoSomethingAsync(impl, context));   
    }
    

    这样,异常不会立即传播,而是包装在Task 中并由异步方法返回。

    为了让它更好,你可以创建一个Decorator 并在那里实现该方法为async。然后在调用方法之前用你的装饰器装饰所有其他实现。

    另一方面,如果您可以修改实现,则 servy 的答案就是要走的路。

    【讨论】:

    • 聪明的主意,我喜欢这个!我只是设法将其分解为一行:tasks.Add(Task.Run(async () =&gt; await impl.DoSomethingAsync(context)));
    • @cpt.jazz:我认为Task.Run 在这里不合适;您正在将执行上下文更改为线程池并对其施加不必要的压力。
    • @Stephen:那么,当我想避免使用单独的方法时,如何改进呢?
    • @cpt.jazz:如果您不想直接调用它(并捕获异常),并且您不想要单独的方法,那么您必须使用 lambda并称之为内联。不幸的是,我相信 lambdas 需要调用强制转换,这使得语法非常尴尬:tasks.Add(((Func&lt;Task&gt;)(async () =&gt; await impl.DoSomethingAsync(context)))());
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多