Преимущества асинхронных методов SqlClient - PullRequest
0 голосов
/ 10 марта 2019

Каковы преимущества нативных *Async методов, доступных в пространстве имен System.Data.SqlClient? Каковы их преимущества перед руководством Task.Run с телом, состоящим только из синхронных вызовов методов?

Вот мой пример «отправной точки» (консольное приложение):

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;
            }
        }
    }
}

Говоря о производительности моего механизма разработки, использование *Async методов фактически привело к снижению времени работы. Как правило, мой вывод был следующим:

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

Другими словами, метод ExecuteNativeAsync - это метод, использующий *Async методы System.Data.SqlClient, и он чаще всего медленнее, чем синхронный метод, заключенный в вызов Task.Run.

Я что-то не так делаю? Может быть, я неправильно читаю документацию?

Ответы [ 3 ]

1 голос
/ 10 марта 2019

Чтобы понять преимущества Async, вам необходимо смоделировать сервер под большой нагрузкой с помощью асинхронных операций, выполнение которых занимает некоторое время.Практически невозможно измерить преимущества приложения, работающего в производственной среде, без написания двух его версий.

Вместо вызова базы данных, которая снова не загружена и предположительно является локальной для приложения,вы можете смоделировать ожидаемые задержки запроса.

По мере увеличения числа клиентов или увеличения продолжительности операции ExecuteAsync значительно превзойдет ExecuteSync.Без нагрузки преимущества использования Async не наблюдаются, что обычно имеет место для большинства приложений, работающих на большинстве серверов.

Преимущество Async в том, что он освобождает поток обратно в пулдо завершения асинхронной операции, освобождая системные ресурсы.

Программа тестирования:

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);
}

Мои результаты:

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.
1 голос
/ 10 марта 2019

Практически во всех сценариях, если вы используете API Sync или Async SqlClient, абсолютно не окажет существенное влияние на время выполнения вашего запроса, совокупное использование ресурсов, пропускную способность приложения или масштабируемость.

Простой факт заключается в том, что ваше приложение, вероятно, не выполняет много тысяч одновременных вызовов SQL Server, и поэтому блокировка потока пула потоков для каждого запроса SQL не является большой проблемой. Это может быть даже полезно, сглаживая пики в объеме запроса.

API полезен, если вы хотите организовать несколько вызовов SQL Server из одного потока. Например, вы можете легко запустить запрос для каждого из N серверов SQL, а затем подождать () результатов.

В современном ASP.NET ваши контроллеры и почти все вызовы API являются асинхронными, а в приложении с пользовательским интерфейсом полезно использовать асинхронные методы, избегая блокировки потока пользовательского интерфейса.

0 голосов
/ 10 марта 2019

Я изменил приведенный выше пример и смог получить выгоду от использования *Async методов:

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;
            }
        }
    }
}

Теперь я получаю следующий вывод:

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

Спасибо Дэвиду Брауну за подсказку!

...