为什么 DbConnection.OpenAsync(CancelToken) 的默认实现是同步的

本文关键字:默认 实现 同步 DbConnection OpenAsync CancelToken 为什么 | 更新日期: 2023-09-27 18:31:23

我正在阅读DbConnection.OpenAsync(CancellationToken)的文档,并发现了以下代码片段:

默认实现调用同步 Open 调用并返回已完成的任务。如果传递了已取消的 cancelToken,则默认实现将返回已取消的任务。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()

为什么 DbConnection.OpenAsync(CancelToken) 的默认实现是同步的

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

为什么默认实现是这样的

一言以蔽之:向后兼容。在理想的世界中,ConnectAsync将是一种抽象的方法;但是,这是不可能的,因为当async出现时,已经有许多DbConnection实现。

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

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

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

不幸的是,这是不可能的。您必须将基类型或接口上的 Task -返回成员视为可能异步的含义。

文档中的关键短语是

提供程序应使用适当的实现进行重写。

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

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

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

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