Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I make SqlDataReader.ReadAsync() run asynchronously?

When making calls to SQL Server that actually do things that take time, SqlDataReader.ReadAsync() runs synchronously for me. Is there any way to force it to run asynchronously or is my only option to call it in Task.Run()?

Here is a repro. It uses winforms to demonstrate that the call blocks the GUI thread. Note that the T-SQL has to actually do something—this is not reproducible with 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);
    }
}

When I run this, the window becomes “(Not Responding)” for 20 seconds before reporting it is done (note that when debugging in VS, “(Not Responding)” text does not appear but it still freezes the same). If I debug in VS and break it while it’s frozen, I see it sitting with a call stack that looks like this:

    [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] 

(further trimmed for brevity).

The whole ReadSyncOverAsync stuff looks particularly suspicious to me. It’s like the SqlClient is assuming a synchronous read will not block, as if it doesn’t know how to use non-blocking IO or something. Yet when viewing reference source or decompiling with JustDecompile, it looks like there’s supposed to be async support but it just somehow heuristically/fallbackedly decided not to use it.

So, how do I get the *Async() stuff in SqlClient to actually be async? I thought that these methods were supposed to enable me to write thread-free responsive GUI programs without needing to use Task.Run() because wrapping synchronous things in Task.Run() just to make them asynchronous is pointless overhead…?

I’m using .net-4.7.02542.

I’m assuming this is a .net bug and have submitted connect #3139210 (EDIT: connect is dead, I have a repro project at https://github.com/binki/connect3139210).

UPDATE: Microsoft acknowledges the bug and will fix it in .net-4.7.3. I used a “Technical Support” case from a VS subscription to report the bug and get this information.

like image 339
binki Avatar asked Aug 09 '17 18:08

binki


1 Answers

Microsoft released a fix for this issue in .net-4.8. I have tested and verified that it works. I have not seen a version of .net-4.7.3 yet, so I do not know if that actually will contain the fix.

The relevant SKUs from regedit for releaseKey=528040:

regedit showing <code>.NETFramework,Version=v4.7.2</code> followed by <code>.NETFramework,Version=v4.8</code>, demonstrating that <code>.NETFramework,Version=4.7.3</code> is absent

Your application must target .net-4.8 to get the fix (merely installing the update does not fix already-compiled applications). Unfortunately, there is no documented <AppContextSwitchOverrides/> for this feature, so you cannot opt into the fix if you must continue targeting an older version of .net. (However, you can target .net-4.8 at compile time, edit the «ProgramName».config to change the <supportedRuntime/>, and then be careful not to use any APIs introduced in .net after the version you’re targeting).

like image 98
binki Avatar answered Oct 07 '22 13:10

binki