16

Edit: This boils down to why does changing just SqlConnection.Open() to await SqlConnection.OpenAsync() within asynchronous code result in strongly different behavior.

What's the difference between a SqlConnection.Open call in a synchronous code and an await SqlConnection.OpenAsync call in an asynchronous code aside from the obvious asynchronous behavior? Is the underlying connection made asynchronous with the database?

The documentation on OpenAsync is lite, https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.openasync%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396.

An asynchronous version of Open, which opens a database connection with the settings specified by the ConnectionString. This method invokes the virtual method OpenAsync with CancellationToken.None.(Inherited from DbConnection.)

I find it interesting that previously the connection string required async=true within it, while in .net 4.5+ it's no longer required. Do the connections behave differently?

https://msdn.microsoft.com/en-us/library/hh211418(v=vs.110).aspx

Beginning in the .NET Framework 4.5, these methods no longer require Asynchronous Processing=true in the connection string.

When I happen to use the synchronous SqlConnection.Open within an asynchronous application and load it heavily I find that it performs very poorly, running the connection pool dry early. I expected opening the connection to be blocking, however, executing asynchronous commands (through dapper) on those connections behaves differently. So, what is OpenAsync doing differently?

EDIT:

As requested code to reproduce the issue (or perhaps demonstrate a difference). Running this case with Open() connection timeouts are encountered at around 180 concurrent async commands executing, with OpenAsync() no exceptions are encountered even at over 300 concurrent commands. You can push the concurrency to eventually get it to timeout, but it's definitely doing it much deeper into the concurrent commands.

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dapper;
using Nito.AsyncEx;

namespace AsyncSqlConnectionTest
{
    class Program
    {
        public static int concurrent_counter = 0;
        public static int total_counter = 0;

        static void Main(string[] args)
        {


            var listToConsume = Enumerable.Range(1, 10000).ToList();
            Parallel.ForEach(listToConsume,
                new ParallelOptions { },
                value =>
                {
                    try
                    {

                        Task.Run(() => AsyncContext.Run(async () =>
                        {
                            using (var conn = new SqlConnection("Data Source=.; Database=master; Trusted_Connection=True;"))
                            {
                                Interlocked.Increment(ref concurrent_counter);
                                Interlocked.Increment(ref total_counter);
                                await conn.OpenAsync();
                                var result = await conn.QueryAsync("select * from master..spt_values; waitfor delay '00:00:05'");
                                Console.WriteLine($"#{total_counter}, concurrent: {concurrent_counter}");
                                Interlocked.Decrement(ref concurrent_counter);
                            }
                        })).GetAwaiter().GetResult();
                    }
                    catch (Exception e)
                    {
                        Console.Write(e.ToString());
                    }
                });
            Console.ReadLine();
        }
    }
}

EDIT 2:

Here's a test which finds the same differences using nothing but ADO.NET. It's worth noting that Dapper executes much faster, but that's not the point here. Again OpenAsync will eventually get a timeout, but much 'later' and never if the max degree of parallelism is 100 (below the connection pool size).

using System;
using System.Data.SqlClient;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace AsyncSqlConnectionTest
{
    class Program
    {
        public static int concurrent_counter = 0;
        public static int total_counter = 0;

        static void Main(string[] args)
        {
            var listToConsume = Enumerable.Range(1, 10000).ToList();
            Parallel.ForEach(listToConsume,
                new ParallelOptions { },
                value =>
                {
                    try
                    {

                        Task.Run(async () =>
                        {
                            using (var conn = new SqlConnection("Data Source=.; Database=master; Trusted_Connection=True;"))
                            {
                                Interlocked.Increment(ref concurrent_counter);
                                Interlocked.Increment(ref total_counter);

                                // this (no errors)
                                await conn.OpenAsync();

                                // vs. this (timeouts)
                                //conn.Open();

                                var cmd = new SqlCommand("select * from master..spt_values; waitfor delay '00:00:05'", conn);
                                using (var reader = await cmd.ExecuteReaderAsync())
                                {
                                    while (await reader.ReadAsync()) { }
                                }
                                Console.WriteLine($"#{total_counter}, concurrent: {concurrent_counter}");
                                Interlocked.Decrement(ref concurrent_counter);
                            }
                        }).GetAwaiter().GetResult();
                    }
                    catch (Exception e)
                    {
                        Console.Write(e.ToString());
                    }
                });
            Console.ReadLine();
        }
    }
}
27
  • 6
    Starting with .NET 4.5 internally it's always async IO. The sync version just blocks...
    – usr
    Commented Nov 17, 2016 at 18:52
  • 1
    running the connection pool dry early it should not behave differently. Whether the thread blocks on IO or on an event makes little difference. executing asynchronous commands ... on those connections behaves differently This might be the case, I don't know. Asynchronous Processing=true must have had some effect in previous .NET versions. I think the wrong question is to look at Open(Async) and the right one is to look at what Asynchronous Processing=true did. But according to this it should at most have had a small CPU cost impact: stackoverflow.com/a/7852617/122718
    – usr
    Commented Nov 17, 2016 at 18:57
  • 1
    I believe not but you should test it in a micro benchmark. Spin up some workload, pause the debugger a few times and look at the Parallel Stacks window. It is very enlightening to watch internal call stacks of libraries to understand what code has impact on throughput. Starting with .NET 4.5 you should see all threads blocked on a task/event for sync APIs and almost no threads active in the async case.
    – usr
    Commented Nov 17, 2016 at 19:00
  • 2
    Is there a concrete issue you are having in your application? If not that's fine but if yes that might be easier to answer.
    – usr
    Commented Nov 17, 2016 at 19:01
  • 2
    The case interests me now. I do not see a good reason there should be any difference. An idea: If Open blocks internally and the thread pool is totally overloaded (here it clearly is) then blocking might take a long time because completing the task being blocked on might require TP resources for IO completion processing. Set the TP to 5000 min/max threads and set MaxDOP for Parallel to 500. The difference should disappear.
    – usr
    Commented Nov 17, 2016 at 19:51

2 Answers 2

4

Open() is a synchronous process which freezes the UI whereas OpenAsync() is an asynchronous process which opens the connection without freezing the UI

    public static async Task<SqlConnection> GetConnectionAsync()   
{  
      var con = new SqlConnection(ConnectionString);   
      if (con.State != ConnectionState.Open)   
          await con.OpenAsync();  
      return con;  
}  
  
public async Task<int> ExecuteQueryAsync(SqlConnection con, string query)  
{  
      if (con == null) con = await GetConnectionAsync();   
      var cmd = new SqlCommand(query, con);   
      return await cmd.ExecuteNonQueryAsync();   
}  
1
  • 2
    The condition if (con.State != ConnectionState.Open) should be if (con.State == ConnectionState.Closed) because it is not a good idea to try open next connection when current one is for example in state Connecting. Source: ConnectionState Enum Commented Jan 30, 2022 at 22:40
0

Looking at the SqlConnection source codes from the new Microsoft.Data.SqlClient it looks like OpenAsync works synchronously. It just implements the retry logic async-ly, and adds cancellation token support (ish) around the sync overload, that's it.

Not the answer you're looking for? Browse other questions tagged or ask your own question.