如何使SqlDataReader.ReadAsync()异步运行?

bin*_*nki 5 c# ado.net sqlclient task-parallel-library

实际执行需要时间的SQL Server调用时,SqlDataReader.ReadAsync()对我来说是同步运行的。有什么方法可以强制它异步运行,还是我调用它的唯一选择Task.Run()?

这是一个复制品。它使用winforms演示该调用阻塞了GUI线程。请注意,T-SQL必须实际执行某项操作-使用不能重现WAITFOR DELAY '00:00:20'。

using System;
using System.Configuration;
using System.Data.Common;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Windows.Forms;

static class SqlDataReaderReadAsyncProgram
{
    static async void Form_Shown(object sender, EventArgs e)
    {
        var form = (Form)sender;
        // Declare your connection string in app.config like
        // <connectionStrings><remove name="LocalSqlServer"/><add name="LocalSqlServer" connectionString="Data Source=localhost\SQLEXPRESS;Integrated Security=true"/></connectionStrings>
        using (DbConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings[0].ConnectionString))
        {
            form.Text = "connecting…";
            await connection.OpenAsync();
            form.Text = "connected!";
            // Install a stored procedure.
            using (var command = connection.CreateCommand())
            {
                command.CommandText = "SET NOCOUNT ON"
                    + " SELECT 'a'"
                    + " DECLARE @t DATETIME = SYSDATETIME()"
                    + " WHILE DATEDIFF(s, @t, SYSDATETIME()) < 20 BEGIN"
                    + "   SELECT 2 x INTO #y"
                    + "   DROP TABLE #y"
                    + " END"
                    + " SELECT 'b'";
                form.Text = "executing…";
                using (var reader = await command.ExecuteReaderAsync())
                {
                    form.Text = "reading…";
                    do
                    {
                        // Blocks on the second call until the second resultset is returned by SQL Server
                        while (await reader.ReadAsync())
                        {
                        }
                    } while (await reader.NextResultAsync());
                    form.Text = "done!";
                }
            }
        }
        await Task.Delay(TimeSpan.FromSeconds(5));
        form.Close();
    }

    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        var form = new Form();
        form.Shown += Form_Shown;
        Application.Run(form);
    }
}
Run Code Online (Sandbox Code Playgroud)

当我运行它时,在报告完成之前,该窗口将变为“(无响应)” 20秒钟(请注意,在VS中进行调试时,“(无响应)”文本不会出现,但仍会冻结)。如果我在VS中调试并在冻结时将其破坏,我会看到它与如下所示的调用堆栈一起坐着:

    [Managed to Native Transition]  
    System.Data.dll!SNINativeMethodWrapper.SNIReadSyncOverAsync(System.Runtime.InteropServices.SafeHandle pConn, ref System.IntPtr packet, int timeout) Unknown
    System.Data.dll!System.Data.SqlClient.TdsParserStateObject.ReadSniSyncOverAsync()   Unknown
    System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryReadNetworkPacket()   Unknown
    System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryPrepareBuffer()   Unknown
    System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryReadByteArray(byte[] buff, int offset, int len, out int totalRead)    Unknown
    System.Data.dll!System.Data.SqlClient.TdsParserStateObject.TryReadInt64(out long value) Unknown
    System.Data.dll!System.Data.SqlClient.TdsParser.TryProcessDone(System.Data.SqlClient.SqlCommand cmd, System.Data.SqlClient.SqlDataReader reader, ref System.Data.SqlClient.RunBehavior run, System.Data.SqlClient.TdsParserStateObject stateObj)    Unknown
    System.Data.dll!System.Data.SqlClient.TdsParser.TryRun(System.Data.SqlClient.RunBehavior runBehavior, System.Data.SqlClient.SqlCommand cmdHandler, System.Data.SqlClient.SqlDataReader dataStream, System.Data.SqlClient.BulkCopySimpleResultSet bulkCopyHandler, System.Data.SqlClient.TdsParserStateObject stateObj, out bool dataReady)  Unknown
    System.Data.dll!System.Data.SqlClient.SqlDataReader.TryHasMoreRows(out bool moreRows)   Unknown
    System.Data.dll!System.Data.SqlClient.SqlDataReader.TryReadInternal(bool setTimeout, out bool more) Unknown
    System.Data.dll!System.Data.SqlClient.SqlDataReader.ReadAsync.AnonymousMethod__0(System.Threading.Tasks.Task t) Unknown
    System.Data.dll!System.Data.SqlClient.SqlDataReader.InvokeRetryable<bool>(System.Func<System.Threading.Tasks.Task, System.Threading.Tasks.Task<bool>> moreFunc, System.Threading.Tasks.TaskCompletionSource<bool> source, System.IDisposable objectToDispose)   Unknown
    System.Data.dll!System.Data.SqlClient.SqlDataReader.ReadAsync(System.Threading.CancellationToken cancellationToken) Unknown
    System.Data.dll!System.Data.Common.DbDataReader.ReadAsync() Unknown
>   SqlDataReaderReadAsync.exe!SqlDataReaderReadAsyncProgram.Form_Shown(object sender, System.EventArgs e) Line 36  C#
    [Resuming Async Method] 
Run Code Online (Sandbox Code Playgroud)

(为简洁起见,还进行了修剪)。

整个ReadSyncOverAsync东西对我来说尤其可疑。就像SqlClient假定同步读取不会阻塞一样,好像它不知道如何使用非阻塞IO一样。但是,当查看参考源或使用JustDecompile进行反编译时,似乎应该有异步支持,但是它以某种方式启发/后备地决定不使用它。

那么,如何使Async()SqlClient中的* 内容实际上是异步的?我认为这些方法应该使我能够编写无需线程的无响应GUI程序,而无需使用它们,Task.Run()因为将同步内容包装为Task.Run()仅使它们异步是没有意义的开销……?

我正在使用.net-4.7.02542。

我假设这是一个.net错误,并提交了连接#3139210(编辑:连接已死,我在https://github.com/binki/connect3139210上有一个repro项目)。

更新:Microsoft确认该错误,并将在.net-4.7.3中修复它。我使用VS订阅中的“技术支持”案例来报告错误并获取此信息。

bin*_*nki 1

Microsoft 在 .net-4.8 中发布了针对此问题的修复程序。我已经测试并验证它有效。我还没有看到 .net-4.7.3 的版本,所以我不知道它是否真的包含修复程序。

\n\n

来自 regedit 的 releaseKey=528040 的相关 SKU:

\n\n

regedit 显示 <code>.NETFramework,Version=v4.7.2</code> 后跟 <code>.NETFramework,Version=v4.8</code>,表明 <code>.NETFramework,Version=4.7.3</code>代码> 不存在

\n\n

您的应用程序必须以 .net-4.8 为目标才能获得修复(仅安装更新并不能修复已编译的应用程序)。不幸的是,没有<AppContextSwitchOverrides/>关于此功能的文档,因此如果您必须继续针对旧版本的 .net,则无法选择修复。(但是,您可以在编译时定位 .net-4.8,编辑 来\xc2\xabProgramName\xc2\xbb.config更改<supportedRuntime/>,然后小心不要使用您\xe2\x80\x99 目标版本之后的 .net 中引入的任何 API)。

\n