【问题标题】:Benefits of async SqlClient methods异步 SqlClient 方法的好处
【发布时间】:2019-08-01 03:33:00
【问题描述】:

System.Data.SqlClient 命名空间中可用的本机 *Async 方法有什么好处?与仅包含同步方法调用的主体的手动 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 是使用System.Data.SqlClient*Async 方法的方法,并且通常比由Task.Run 调用包装的同步方法慢。

我做错了吗?也许我误读了文档?

【问题讨论】:

  • 为什么你认为异步运行一个方法会使其更快?
  • 您的结果显示 ExecuteSync 最慢。如果您只是要立即await 它,那么调用 Async 方法并没有多大意义。关键是你可以在它执行的时候做其他事情。
  • @stuartd 我认为不应该。我通常对其他可能的好处感兴趣。例如,可以想象一个迁移场景。切换到*Async 有什么好处?在性能方面,我没有看到任何好处。另外还有更多的代码重写。但是,也许还有其他好处?我很感兴趣那些可能是什么,就是这样。
  • @Kerido 使用async 的重点更多是关于服务器处于压力下时的可伸缩性;在低负载下,async 会比普通的同步调用增加更多的开销,但实际上async 增加的小开销在服务器负载过重时是值得的。
  • @Blorgbeard 我正在尝试实现一个“实用的数据库读取场景”,很难想象与此数据库读取相关的行之间的任何侧面逻辑。你能不能想一想?

标签: c# sql-server async-await ado.net


【解决方案1】:

要了解异步的好处,您需要使用需要一些时间才能完成的异步操作来模拟负载较重的服务器。如果不编写两个版本的应用程序,几乎不可能衡量在生产环境中运行的应用程序的好处。

您可以模拟预期的查询延迟,而不是调用再次处于无负载状态且可能是应用程序本地的数据库。

随着客户端数量或操作长度的增加ExecuteAsync 将显着优于ExecuteSync。在无负载情况下,没有观察到使用异步的好处,这通常是大多数服务器上运行的大多数应用程序的情况。

这里使用异步的好处是它会将线程释放回池中,直到异步操作完成,从而释放系统资源。

测试程序:

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.

【讨论】:

  • @Kerido 即使你找到了答案,我也完成了这篇文章。我很好奇异步在什么时候比同步有好处。我希望这对你也有帮助。
【解决方案2】:

我已经修改了上面的示例,并且能够从使用 *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.

感谢大卫布朗的提示!

【讨论】:

    【解决方案3】:

    在几乎所有场景中,无论您使用 Sync 还是 Async SqlClient API,绝对没有对您的查询运行时、聚合资源利用率、应用程序吞吐量或可扩展性产生有意义的影响。

    简单的事实是,您的应用程序可能不会进行数千个并发 SQL Server 调用,因此为每个 SQL 查询阻塞线程池线程并不是什么大问题。它甚至可以通过消除请求量的峰值来带来好处。

    如果您想从单个线程编排多个 SQL Server 调用,该 API 非常有用。例如,您可以轻松地启动对 N 个 SQL Server 中的每一个的查询,然后 Wait() 以获取结果。

    在现代 ASP.NET 中,您的控制器和几乎所有 API 调用都是异步的,在 UI 应用程序中,使用异步方法非常有用,可以避免阻塞 UI 线程。

    【讨论】:

    • 我对这种说法感到惊讶:“对...应用程序吞吐量或可伸缩性绝对没有有意义的影响”。您的意思是,在异步 Web api 方法中,调用 SqlCommand.ExecuteNonQuery 与在 SqlCommand.ExecuteNonQueryAsync 上等待不会对应用程序的可扩展性产生任何影响?谢谢。
    • 我是否可以通过并行执行完全相同的逻辑来修改我的示例,以假装我正在为来自多个“虚拟客户端”的请求提供服务?
    • @yv989c 是的。您通常有大量可用线程,并且为每个 SQL 调用阻塞一个线程没什么大不了的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-03
    • 1970-01-01
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    • 2019-03-19
    相关资源
    最近更新 更多