22

在切换到新的 .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");
    }
}

被捕获的是DisposeAsync-exception 如果它被抛出,并且await using只有内部的异常DisposeAsync没有抛出。

但是,我更喜欢另一种方式:await using如果可能,从块中获取异常,并且 -DisposeAsync仅当await using块成功完成时才出现异常。

理由:假设我的班级D使用一些网络资源并订阅了一些远程通知。里面的代码await using可能会出错并导致通信通道失败,然后在 Dispose 中尝试优雅地关闭通信(例如,取消订阅通知)的代码也会失败。但是第一个例外为我提供了有关问题的真实信息,而第二个例外只是次要问题。

在另一种情况下,当 main 部分运行并且处理失败时,真正的问题在 inside DisposeAsync,因此异常 fromDisposeAsync是相关的。这意味着仅仅抑制内部的所有异常DisposeAsync不应该是一个好主意。


我知道非异步 case 也存在同样的问题:异常 infinally覆盖了异常 in try,这就是为什么不建议 throw 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 { }
        }
    }
}

并像使用它一样

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

这有点丑陋(并且不允许在 using 块内提前返回之类的事情)。

如果可能的话,是否有一个好的、规范的解决方案await using?我在互联网上的搜索甚至没有发现讨论这个问题。

4

4 回答 4

5

也许您已经明白为什么会发生这种情况,但值得一提。此行为并非特定于await using. 它也会发生在一个普通的using块上。所以当我Dispose()在这里说的时候,这一切都适用于DisposeAsync()

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

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

try {
    throw new Exception("Inside try");
} finally {
    throw new Exception("Inside finally");
}

在. _ Dispose()_ 行为是相同的。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
}

或者你不能使用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
    }
}
于 2019-11-20T13:36:12.680 回答
5

有一些你想暴露的异常(中断当前请求,或关闭进程),你的设计期望有时会出现一些异常,你可以处理它们(例如重试并继续)。

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

有时,调用者会更优先考虑从原始代码块中显示异常,有时是来自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;
}
于 2019-12-04T22:40:09.923 回答
3

using 是有效的异常处理代码(try...finally...Dispose() 的语法糖)。

如果您的异常处理代码正在抛出异常,那么某些东西就会被严重破坏。

无论发生什么甚至让你进入那里,都不再重要了。错误的异常处理代码将以一种或另一种方式隐藏所有可能的异常。异常处理代码必须是固定的,具有绝对优先级。没有它,您将永远无法获得足够的调试数据来解决真正的问题。我经常看到它做错了。它和处理裸指针一样容易出错。通常,有两篇关于我链接的主题的文章,可能会帮助您解决任何潜在的设计误解:

根据异常分类,如果您的异常处理/处理代码抛出异常,您需要执行以下操作:

对于 Fatal、Boneheaded 和 Vexing,解决方案是相同的。

外生异常,即使付出巨大代价也必须避免。我们仍然使用日志文件而不是日志数据库来记录异常是有原因的——DB Opeartions 只是容易遇到外部问题的方法。日志文件是一种情况,我什至不介意您是否让文件句柄在整个运行时打开。

如果您必须关闭连接,请不要太担心另一端。像 UDP 一样处理它:“我会发送信息,但我不在乎对方是否得到它。” 处置是关于清理您正在处理的客户端/端的资源。

我可以尝试通知他们。但是清理服务器/FS端的东西?这就是他们的超时和异常处理责任。

于 2019-12-02T17:18:39.397 回答
1

您可以尝试使用 AggregateException 并修改您的代码,如下所示:

class Program 
{
    static async Task Main()
    {
        try
        {
            await using (var d = new D())
            {
                throw new ArgumentException("I'm inside using");
            }
        }
        catch (AggregateException ex)
        {
            ex.Handle(inner =>
            {
                if (inner is Exception)
                {
                    Console.WriteLine(e.Message);
                }
            });
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

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

https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8

https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library

于 2019-12-05T09:01:37.687 回答