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

Posted

技术标签:

【中文标题】如何使 SqlDataReader.ReadAsync() 异步运行?【英文标题】:How do I make SqlDataReader.ReadAsync() run asynchronously? 【发布时间】:2018-01-17 18:36:22 【问题描述】:

当调用实际执行需要时间的事情的 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);
    

当我运行它时,窗口在报告完成之前变为“(未响应)”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] 

(为简洁起见进一步修剪)。

整个ReadSyncOverAsync 的东西在我看来特别可疑。就像 SqlClient 假设同步读取不会阻塞一样,好像它不知道如何使用非阻塞 IO 什么的。然而,当查看参考源或使用 JustDecompile 进行反编译时,看起来应该有异步支持,但它只是以某种方式启发式/回退地决定不使用它。

那么,如何让 SqlClient 中的 *Async() 内容真正异步?我认为这些方法应该使我能够编写无线程响应式 GUI 程序,而无需使用 Task.Run(),因为将同步的东西包装在 Task.Run() 中只是为了使它们异步是毫无意义的开销……?

我正在使用 .net-4.7.02542。

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

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

【问题讨论】:

如果在查询开头添加SET NOCOUNT ON;,它会停止锁定吗?这可能只是您紧密循环中的行计数报告的间接费用。 @ScottChamberlain 如果我犯了这样的错误,我不会感到惊讶。但是,将SET NOCOUNT ON 添加到CommandText 的开头并没有什么不同。我很难在调试器中弄清楚ReadSniSyncOverAsync() 是否真的阻塞了,它可能一直处于同步繁忙循环中。 按钮点击事件和Form.Show事件有区别吗? @Igor 不,与Button.Click 的行为完全相同。 【参考方案1】:

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

regedit 中 releaseKey=528040 的相关 SKU:

您的应用程序必须以 .net-4.8 为目标才能获得修复(仅安装更新并不能修复已编译的应用程序)。不幸的是,此功能没有记录在案的&lt;AppContextSwitchOverrides/&gt;,因此如果您必须继续针对较旧版本的 .net,则无法选择修复。 (但是,您可以在编译时以 .net-4.8 为目标,编辑 «ProgramName».config 以更改 &lt;supportedRuntime/&gt;,然后注意不要使用您所针对的版本之后在 .net 中引入的任何 API)。

【讨论】:

以上是关于如何使 SqlDataReader.ReadAsync() 异步运行?的主要内容,如果未能解决你的问题,请参考以下文章

如何使 RelativeLayout 半透明但不使活动

如何用word使图片上下居中

如何使图像自动调整大小,使宽度为 100% 并相应调整高度?

如何使 UISegmentedcontrol 透明?

如何使 textarea 填充 div 块?

如何使 UITableViewCell 显示为禁用?