在AsyncDispose中处理异常的正确方法

Vla*_*lad 6 c# async-await iasyncdisposable

在切换到新的.NET Core 3的过程中IAsynsDisposable,我偶然发现了以下问题。

问题的核心:如果DisposeAsync引发异常,则此异常隐藏await using-block 内部引发的所有异常。

class Program 
{
    static async Task Main()
    {
        try
        {
            await using (var d = new D())
            {
                throw new ArgumentException("I'm inside using");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // prints I'm inside dispose
        }
    }
}

class D : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(1);
        throw new Exception("I'm inside dispose");
    }
}
Run Code Online (Sandbox Code Playgroud)

被捕获的是AsyncDispose-exception(如果引发了),而内部异常await using只有在AsyncDispose未引发的情况下才被捕获。

但是,我还是更喜欢它:await using如果可能的话,从块中获取异常,并且DisposeAsync仅当await using块成功完成时才使用-exception 。

基本原理:想象一下,我的课D使用了一些网络资源,并预订了一些远程通知。内部代码await using可能会出错,并导致通信通道失败,此后,Dispose中试图正常关闭通信(例如,取消订阅通知)的代码也会失败。但是第一个例外为我提供了有关该问题的真实信息,第二个例外只是次要问题。

在另一种情况下,当主要部分通过并且处置失败时,真正的问题就在内部DisposeAsync,因此相关的例外DisposeAsync。这意味着仅仅抑制内部的所有异常DisposeAsync并不是一个好主意。


我知道非异步情况也存在相同的问题:in中finally的exception覆盖in中的exception try,这就是为什么不建议使用in的原因Dispose()。但是,通过网络访问类,抑制关闭方法中的异常看起来一点也不好。


可以使用以下帮助程序解决此问题:

static class AsyncTools
{
    public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
            where T : IAsyncDisposable
    {
        bool trySucceeded = false;
        try
        {
            await task(disposable);
            trySucceeded = true;
        }
        finally
        {
            if (trySucceeded)
                await disposable.DisposeAsync();
            else // must suppress exceptions
                try { await disposable.DisposeAsync(); } catch { }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并像这样使用

await new D().UsingAsync(d =>
{
    throw new ArgumentException("I'm inside using");
});
Run Code Online (Sandbox Code Playgroud)

这有点丑陋(并且在using块中不允许类似早日返回之类的事情)。

是否有一个好的,规范的解决方案,await using如果可能的话?我在互联网上的搜索甚至都没有找到讨论此问题的信息。

Dan*_*ker 6

有些异常是您想要显示的(中断当前请求,或停止进程),有些异常是您的设计预期有时会发生并且您可以处理它们(例如重试并继续)。

但是区分这两种类型取决于代码的最终调用者 - 这就是异常的全部要点,将决定权留给调用者。

有时,调用者会优先考虑显示原始代码块中的异常,有时会优先考虑Dispose. 没有通用规则来决定哪个应该优先。CLR 至少在同步和非异步行为之间是一致的(正如您所注意到的)。

也许不幸的是,现在我们必须AggregateException表示多个异常,无法对其进行改造来解决这个问题。即,如果一个异常已经在运行,并且抛出了另一个异常,那么它们将被组合成一个AggregateException. 可以修改该catch机制,以便如果您编写catch (MyException),它将捕获任何AggregateException包含类型异常的内容MyException。不过,这个想法还带来了各种其他并发症,而且现在修改如此基本的东西可能风险太大。

您可以改进您的UsingAsync以支持提前返回值:

public static async Task<R> UsingAsync<T, R>(this T disposable, Func<T, Task<R>> task)
        where T : IAsyncDisposable
{
    bool trySucceeded = false;
    R result;
    try
    {
        result = await task(disposable);
        trySucceeded = true;
    }
    finally
    {
        if (trySucceeded)
            await disposable.DisposeAsync();
        else // must suppress exceptions
            try { await disposable.DisposeAsync(); } catch { }
    }
    return result;
}
Run Code Online (Sandbox Code Playgroud)


Gab*_*uci 5

也许您已经明白为什么会发生这种情况,但值得详细说明。此行为并非特定于await using. 对于普通using块也会发生这种情况。因此,虽然我Dispose()在这里这么说,但这一切也都适用DisposeAsync()

正如文档的注释部分所述,块using只是try/块的语法糖。您所看到的情况会发生,因为该块始终运行,即使在发生异常之后也是如此。因此,如果发生异常,并且没有块,则异常将被搁置,直到块运行,然后抛出异常。但如果异常发生在 中,您将永远不会看到旧的异常。finallyfinallycatchfinallyfinally

您可以通过以下示例看到这一点:

try {
    throw new Exception("Inside try");
} finally {
    throw new Exception("Inside finally");
}
Run Code Online (Sandbox Code Playgroud)

是否在 内调用Dispose()or并不重要。行为是一样的。DisposeAsync()finally

我的第一个想法是:不要扔进去Dispose()。但在审查了微软自己的一些代码后,我认为这要看情况。

FileStream例如,看看他们的实现。既是同步Dispose()方法,又DisposeAsync()实际上可以抛出异常。同步确实Dispose()故意忽略一些异常,但不是全部。

但我认为考虑班级的性质很重要。FileStream例如,在 a 中,Dispose()会将缓冲区刷新到文件系统。这是一项非常重要的任务,您需要知道是否失败。你不能忽视这一点。

然而,在其他类型的对象中,当您调用 时Dispose(),您确实不再使用该对象了。打电话Dispose()实际上只是意味着“这个对象对我来说已经死了”。也许它会清理一些分配的内存,但失败不会以任何方式影响应用程序的操作。在这种情况下,您可能决定忽略Dispose().

但无论如何,如果您想区分 内部的异常using或来自 的异常Dispose(),那么您需要在块内部和外部都有一个try/块:catchusing

try {
    await using (var d = new D())
    {
        try
        {
            throw new ArgumentException("I'm inside using");
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message); // prints I'm inside using
        }
    }
} catch (Exception e) {
    Console.WriteLine(e.Message); // prints I'm inside dispose
}
Run Code Online (Sandbox Code Playgroud)

或者你可以不使用using. 自己写一个try//catchfinally,在其中捕获任何异常finally

var d = new D();
try
{
    throw new ArgumentException("I'm inside try");
}
catch (Exception e)
{
    Console.WriteLine(e.Message); // prints I'm inside try
}
finally
{
    try
    {
        if (D != null) await D.DisposeAsync();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message); // prints I'm inside dispose
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 顺便说一句,https://source.dot.net (.NET Core) / https://referencesource.microsoft.com (.NET Framework) 比 GitHub 更容易浏览 (3认同)
  • @canton7 太棒了!我知道 https://referencesource.microsoft.com,但不知道有 .NET Core 的等效项。谢谢! (2认同)
  • @PauloMorgado 这只是意味着 `Dispose()` 不应该抛出*因为*它被多次调用。微软自己的实现可以抛出异常,并且有充分的理由,正如我在这个答案中所示的那样。但是,我确实同意您应该尽可能避免它,因为通常没有人会期望它会抛出异常。 (2认同)