【问题标题】:Why is the default implementation of DbConnection.OpenAsync(CancellationToken) synchronous?为什么 DbConnection.OpenAsync(CancellationToken) 的默认实现是同步的?
【发布时间】:2016-01-20 22:32:17
【问题描述】:

我正在阅读documentation for DbConnection.OpenAsync(CancellationToken),发现以下sn-p:

默认实现调用同步的Open 调用并返回一个已完成的任务。如果传递了一个已经取消的cancellationToken,默认实现将返回一个取消的任务。 Open 抛出的异常将通过返回的 Task Exception 属性进行通信。

现在,如果我的网络连接不稳定/缓慢,并且使用的数据库提供程序尚未覆盖 DbConnection.OpenAsync(CancellationToken)(即,我使用的不是 System.Data.SqlClient),并且 如果我将其放入 UI Button 的事件处理程序中,例如:(假设代码,未经测试)

async void button1_Clicked(object sender, EventArgs e)
{
    using (var connection = MyProviderFactory.CreateConnection())
    {
        button1.Text = "Opening…";
        connection.ConnectionString = _SomeString;
        try
        {
            await connection.OpenAsync(default);
            button1.Text = "Opened successfully!";
        }
        catch (Exception ex)
        {
            button1.Text = ex.Message;
        }
    }
}

根据我引用的文档,如果连接需要足够长的时间才能完成,如果提供者没有覆盖默认实现,则在建立连接时我的表单将是“(未响应)”。无论底层数据库提供商如何,为了防止这种情况发生,我不妨这样做await Task.Run(async () => await connection.OpenAsync());。为什么默认实现是这种方式?如果不编写提供程序感知代码,应该如何知道何时需要Task.Run()

【问题讨论】:

    标签: c# .net task-parallel-library


    【解决方案1】:

    您的await Task.Run(async () => await connection.OpenAsync()) 不会在同一个线程中执行connection.OpenAsync(),但connection.OpenAsync()connection.Open() 依赖于线程本地状态是完全合理的。例如,他们可能并且通常应该注意Transaction.Current。如果 .NET Framework 在后台线程中静默执行connection.Open(),有些人会得到非常错误的结果。

    【讨论】:

    • 这种隐式事务管理 API 看起来使用起来很危险,而且我喜欢完全避免使用这种 API……
    • 啊,我明白了,你是说因为一些提供者在打开连接时使用线程本地存储,所以DbConnection提供的默认实现无法安全地使用线程池使其立即返回。
    • @binki 就像Transaction.Current 的文档中所说的那样,您通常不会直接使用它。你用TransactionScope来管理它,然后它就变成了一个非常有用的机制。连接类本身将直接使用Transaction.Current,如果在后台线程上调用,则不会看到预期的值。我没有说连接类本身使用线程本地存储(我只考虑其他类),但你是对的,这是另一种有效的可能性。
    • 我仍然认为依赖线程本地存储会以令人惊讶的方式使其变得脆弱。但我这么说是因为我以前从未听说过这种模式/API。现在,如果有类似 nodejs 的 continuation-local-storage 之类的东西来跟踪事务范围,那对我来说可能会更安全一些,尽管它仍然不是直截了当的。有趣的是,范围的东西存在,是的,我可以看到它是如何使代码更加整洁的。
    【解决方案2】:

    为什么默认实现是这样的

    一句话:向后兼容。在理想世界中,ConnectAsync 将是一个抽象方法;然而,这是不可能的,因为当 async 出现时,已经有很多 DbConnection 实现了。

    因此,DbConnection 的设计者必须选择同步或伪异步(线程池)实现。这两种选择都不能提供出色的最终用户体验。

    对于一个有趣的反例,请考虑Stream。这是另一个面临相同问题的常见基类,但做出了相反的选择(即基类Stream.ReadAsync 实现从线程池中调用Stream.Read)。

    在不编写提供程序感知代码的情况下,如何知道何时需要 Task.Run()?

    很遗憾,这是不可能的。您必须将基类型或接口上的Task-returning 成员视为可能异步。

    【讨论】:

      【解决方案3】:

      文档中的关键词是

      提供者应该用适当的实现覆盖。

      DbConnection 是实现特定类的基类。它不知道底层实现,不知道如何使其异步。基类开发人员选择为未实现自己的异步版本的提供者提供简单的实现。

      我同意,这个实现并不是一个很好的实现,但是你可以在不知道底层实现的情况下做所有事情。

      如果 Open 实现不使用网络怎么办?也许它只是打开一个文件。基类无法进行泛化。

      我希望大多数供应商实现此方法的真正异步版本,但如果您真的需要与 任何人 异步,那么我只需将其包装在运行中即可。但是,如果您遇到不支持真正异步打开的提供程序,它很可能也不支持线程安全打开。

      【讨论】:

      • 打开文件仍然是一个阻塞操作,其中存在异步 API,即使人们反对它,在线程池的帮助下“伪造”异步访问也会起作用。
      • 是的,打开文件有异步 API,但 DbConnection 类不知道这是怎么回事。使用文件访问的提供者将有一个微不足道的时间来实现适当的 OpenAsync,对于基类来说这是不可能的。用线程池伪造它会起作用,除非 Open 做了类似将文件句柄放在线程本地存储中的操作,否则它根本不起作用。就我个人而言,如果 Open 可能是一项耗时的操作,我会假设提供者正确实施了 OpenAsync。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-04-29
      • 1970-01-01
      • 2015-04-14
      • 1970-01-01
      • 1970-01-01
      • 2017-05-18
      相关资源
      最近更新 更多