1

What are the benefits of native *Async methods available in the System.Data.SqlClient namespace? What are their advantages over a manual Task.Run with a body comprised of only synchronous method calls?

Here's my 'starting point' example (console application):

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

class Program
{
    const string CommandTest = @"
SET NOCOUNT ON;
WITH
    L0   AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)), -- 2^1
    L1   AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),       -- 2^2
    L2   AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),       -- 2^4
    L3   AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),       -- 2^8
    L4   AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),       -- 2^16
    L5   AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),       -- 2^32
    Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS k FROM L5)
SELECT
    k
FROM
    Nums
WHERE
    k <= 1000000";

    const string ConnectionString = "Server=.;Database=master;Integrated Security=SSPI;";

    // This requires c# 7.1 or later. Check project settings
    public static async Task Main(string[] args)
    {
        var aSW = new System.Diagnostics.Stopwatch();

        aSW.Restart();
        {
            var aRes = ExecuteSync();
            Console.WriteLine($"ExecuteSync         returned {aRes} in {aSW.Elapsed}.");
        }

        aSW.Restart();
        {
            var aRes = await ExecuteWrapperAsync();
            Console.WriteLine($"ExecuteWrapperAsync returned {aRes} in {aSW.Elapsed}.");
        }

        aSW.Restart();
        {
            var aRes = await ExecuteNativeAsync();
            Console.WriteLine($"ExecuteNativeAsync  returned {aRes} in {aSW.Elapsed}.");
        }
    }

    private static Task<long> ExecuteWrapperAsync()
    {
        return Task.Run(() => ExecuteSync());
    }

    private static long ExecuteSync()
    {
        using (var aConn = new SqlConnection(ConnectionString))
        using (var aCmd = new SqlCommand(CommandTest, aConn))
        {
            aConn.Open();

            using (var aR = aCmd.ExecuteReader())
            {
                long aRetVal = 0;

                while (aR.Read())
                    aRetVal += aR.GetInt64(0);

                return aRetVal;
            }
        }
    }

    private static async Task<long> ExecuteNativeAsync()
    {
        using (var aConn = new SqlConnection(ConnectionString))
        using (var aCmd = new SqlCommand(CommandTest, aConn))
        {
            await aConn.OpenAsync();

            using (var aR = await aCmd.ExecuteReaderAsync())
            {
                long aRetVal = 0;

                while (await aR.ReadAsync())
                    aRetVal += aR.GetInt64(0);

                return aRetVal;
            }
        }
    }
}

Speaking about performance on my development maching, usage of the *Async methods actually resulted in slower running times. Typically, my output was as follows:

ExecuteSync         returned 500000500000 in 00:00:00.4514950.
ExecuteWrapperAsync returned 500000500000 in 00:00:00.2525898.
ExecuteNativeAsync  returned 500000500000 in 00:00:00.3662496.

In other words, the method ExecuteNativeAsync is the one using the *Async methods of System.Data.SqlClient and was most often slower than a synchronous method wrapped by a Task.Run call.

Am I doing something wrong? Maybe I am mis-reading the documentation?

Kerido
  • 2,930
  • 2
  • 21
  • 34
  • Why do you think running a method asynchronously would make it faster? – stuartd Mar 10 '19 at 19:17
  • Your results show ExecuteSync being the slowest. And there's not a lot of point calling an Async method if you're just going to immediately `await` it. The point is that you can do other things while it's executing. – Blorgbeard Mar 10 '19 at 19:19
  • @stuartd I don't think it should. I'm generally interested what other benefits might be. For example, one could imagine a migration scenario. What's the benefit of switching to `*Async`? In terms of performance, I see no benefits. Plus there are more code re-writes. But maybe, however, there are other benefits? I'm interested what those might be, that's it. – Kerido Mar 10 '19 at 19:20
  • @Kerido the point of using `async` is more about scalability when the server is under pressure; under low load the reality is that `async` will add more overhead than a plain synchronous call, but in practice the small overhead added by `async` is worth it when the server is under heavy load. – yv989c Mar 10 '19 at 19:22
  • @Blorgbeard I'm trying to implement a 'practical DB read scenario' and it's kind of hard to imagine any side logic in between lines related to this DB read. Can you please think of smth? – Kerido Mar 10 '19 at 19:23
  • @yv989c Do you mean SQL Server or some web server that issues a similar DB read? – Kerido Mar 10 '19 at 19:24
  • Async releases the thread while it waits for the result so the system can reuse it. When you have a production application that has Async calls that don't immediately return this has benefits system wide. What you are measuring here is the difference between calling a database with a single thread and whether you are wrapping it in a Task or not. – Jerry Mar 10 '19 at 19:26
  • @Jerry Then can I somehow modify my example so that the pluses of `*Async` become more obvious? Maybe, switch from measuring performance to measuring something else... – Kerido Mar 10 '19 at 19:28
  • @Kerido, the client is the one taking advantage of `async`. As others pointed out, you can do other things while you wait for the query to complete, and this will be the more obvious example in the case of a console application, but if instead the client is a web application, using `async` increases the scalability of that application because finite resources are made available to other requests while you are awaiting; if you don't await on those Async sufixed api methods, those finite resources are held and other requests cannot make use of them until the operation is completed. – yv989c Mar 10 '19 at 19:36
  • The thing is that Console Applications aren't benefited the most from using `async`/`await`, only if you are going to use `Task.Run` or handling parallel Tasks. – Camilo Terevinto Mar 10 '19 at 19:36
  • Related: https://stackoverflow.com/q/42415969/11683 – GSerg Mar 15 '21 at 20:25

3 Answers3

2

In almost all scenarios whether you use the Sync or Async SqlClient APIs will have absolutely no meaningful impact on your query runtime, aggregate resource utilization, application throughput, or scalability.

The simple fact is that your app is probably not making many thousands of concurrent SQL Server calls, and so blocking a thread pool thread for every SQL Query is not a big deal. It can even be beneficial by smoothing out spikes in request volume.

The API's a useful if you want to orchestrate multiple SQL Server calls from a single thread. For instance you can easily kick off a query to each of N SQL Servers, and then Wait() for the results.

In modern ASP.NET your controllers and almost all of your API calls are async, and in a UI application its a useful to use Async methods do avoid blocking the UI thread.

David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • 1
    I'm surprised by this statement: "absolutely no meaningful impact on... application throughput, or scalability". What are you saying is that in an async web api method, calling `SqlCommand.ExecuteNonQuery` vs awaiting on `SqlCommand.ExecuteNonQueryAsync` won't have any effect on the application scalability? thanks. – yv989c Mar 10 '19 at 19:48
  • Can I then modify my example by executing the exact same logic in parallel to pretend I'm serving requests from multiple 'virtual clients'? – Kerido Mar 10 '19 at 19:49
  • @yv989c yep. You typically have plenty of threads available, and blocking one for each SQL call is no big deal. – David Browne - Microsoft Mar 10 '19 at 22:26
2

To understand the benefits of Async you need to simulate a server under heavy load with asynchronous operations that take some time to complete. It is virtually impossible to measure the benefit in an application that's running in a production environment without writing two versions of it.

Instead of calling a database that again is under no load, and is presumably local to the application, you can simulate your expected query delays.

As the number of clients, or the length of the operation increases ExecuteAsync will significantly outperform ExecuteSync. Under no load the benefits of using Async are not observed, which is usually the case for the majority of applications running on the majority of servers.

The benefit of Async here is that it releases the thread back to the pool until the asynchronous operation completes, freeing system resources.

The test program:

static void Main(string[] args)
{
    RunTest(clients: 10,   databaseCallTime: 10);
    RunTest(clients: 1000, databaseCallTime: 10);
    RunTest(clients: 10,   databaseCallTime: 1000);
    RunTest(clients: 1000, databaseCallTime: 1000);
}

public static void RunTest(int clients, int databaseCallTime)
{ 
    var aSW = new Stopwatch();

    Console.WriteLine($"Testing {clients} clients with a {databaseCallTime}ms database response time.");

    aSW.Restart();
    {
        Task.WaitAll(
            Enumerable.Range(0, clients)
                .AsParallel()
                .Select(_ => ExecuteAsync(databaseCallTime))
                .ToArray());

        Console.WriteLine($"-> ExecuteAsync returned in {aSW.Elapsed}.");
    }

    aSW.Restart();
    {
        Task.WaitAll(
            Enumerable.Range(0, clients)
                .AsParallel()
                .Select(_ => Task.Run(() => ExecuteSync(databaseCallTime)))
                .ToArray());

        Console.WriteLine($"-> ExecuteSync  returned in {aSW.Elapsed}.");
    }

    Console.WriteLine();
    Console.WriteLine();
}

private static void ExecuteSync(int databaseCallTime)
{
    Thread.Sleep(databaseCallTime);
}

private static async Task ExecuteAsync(int databaseCallTime)
{
    await Task.Delay(databaseCallTime);
}

My results:

Testing 10 clients with a 10ms database response time.
-> ExecuteAsync returned in 00:00:00.1119717.
-> ExecuteSync  returned in 00:00:00.0268717.


Testing 1000 clients with a 10ms database response time.
-> ExecuteAsync returned in 00:00:00.0593431.
-> ExecuteSync  returned in 00:00:01.3065965.


Testing 10 clients with a 1000ms database response time.
-> ExecuteAsync returned in 00:00:01.0126014.
-> ExecuteSync  returned in 00:00:01.0099419.


Testing 1000 clients with a 1000ms database response time.
-> ExecuteAsync returned in 00:00:01.1711554.
-> ExecuteSync  returned in 00:00:25.0433635.
Dan
  • 9,717
  • 4
  • 47
  • 65
Jerry
  • 1,477
  • 7
  • 14
  • @Kerido I finished writing this even though you found your answer. I was curious at what point asynchronous would have a benefit over synchronous. I hope this helps you as well. – Jerry Mar 10 '19 at 21:05
1

I have modified the above example and was able to actually benefit from using *Async methods:

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

class Program
{
    const string CommandTest = @"
SET NOCOUNT ON;
WAITFOR DELAY '00:00:01';
WITH
    L0   AS (SELECT c FROM (SELECT 1 UNION ALL SELECT 1) AS D(c)), -- 2^1
    L1   AS (SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B),       -- 2^2
    L2   AS (SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B),       -- 2^4
    L3   AS (SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B),       -- 2^8
    L4   AS (SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B),       -- 2^16
    L5   AS (SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B),       -- 2^32
    Nums AS (SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS k FROM L5)
SELECT
    k
FROM
    Nums
WHERE
    k <= 100000";

    const string ConnectionString = "Server=tcp:.;Database=master;Integrated Security=SSPI;";

    const int VirtualClientCount = 100;

    // This requires c# 7.1 or later. Check project settings
    public static async Task Main(string[] args)
    {
        var aSW = new System.Diagnostics.Stopwatch();

        aSW.Restart();
        {
            var aTasks = Enumerable.Range(0, VirtualClientCount).Select(_ => ExecuteWrapperAsync());
            await Task.WhenAll(aTasks);
            Console.WriteLine($"ExecuteWrapperAsync completed in {aSW.Elapsed}.");
        }

        aSW.Restart();
        {
            var aTasks = Enumerable.Range(0, VirtualClientCount).Select(_ => ExecuteNativeAsync());
            await Task.WhenAll(aTasks);
            Console.WriteLine($"ExecuteNativeAsync  completed in {aSW.Elapsed}.");
        }
    }

    private static Task<long> ExecuteWrapperAsync()
    {
        return Task.Run(() => ExecuteSync());
    }

    private static long ExecuteSync()
    {
        using (var aConn = new SqlConnection(ConnectionString))
        using (var aCmd = new SqlCommand(CommandTest, aConn))
        {
            aConn.Open();

            using (var aR = aCmd.ExecuteReader())
            {
                long aRetVal = 0;

                while (aR.Read())
                    aRetVal += aR.GetInt64(0);

                return aRetVal;
            }
        }
    }

    private static async Task<long> ExecuteNativeAsync()
    {
        using (var aConn = new SqlConnection(ConnectionString))
        using (var aCmd = new SqlCommand(CommandTest, aConn))
        {
            await aConn.OpenAsync();

            using (var aR = await aCmd.ExecuteReaderAsync())
            {
                long aRetVal = 0;

                while (await aR.ReadAsync())
                    aRetVal += aR.GetInt64(0);

                return aRetVal;
            }
        }
    }
}

Now I'm getting the following output:

ExecuteWrapperAsync completed in 00:00:09.6214859.
ExecuteNativeAsync  completed in 00:00:02.2103956.

Thanks to David Browne for the hint!

Kerido
  • 2,930
  • 2
  • 21
  • 34