【问题标题】:Individual await vs Task.WhenAll个人 await vs Task.WhenAll
【发布时间】:2018-05-28 06:34:09
【问题描述】:

我有以下两种方法,产生相同的结果。

public static async Task<IEnumerable<RiskDetails>> ExecuteSqlStoredProcedureSelect<T>(IEnumerable<AccountInfo> linkedAccounts, string connectionString, string storedProcedure, int connTimeout = 10)
{
        var responseList = new List<RiskDetails>();

        using (IDbConnection conn = new SqlConnection(connectionString))
        {
            foreach (var account in linkedAccounts)
            {
                var enumResults = await conn.QueryAsync<RiskDetails>(storedProcedure, 
                    new { UserID = account.UserID, CasinoID = account.CasinoID, GamingServerID = account.GamingServerID, AccountNo = account.AccountNumber, Group = account.GroupCode, EmailAddress = account.USEMAIL }, 
                    commandType: CommandType.StoredProcedure);
                    
                if (enumResults != null)
                        foreach (var response in enumResults)
                            responseList.Add(response);
            }
         }

         return responseList;
    }
        
    public static async Task<IEnumerable<RiskDetails>> ExecuteSqlStoredProcedureSelectParallel<T>(IEnumerable<AccountInfo> linkedAccounts, string connectionString, string storedProcedure, int connTimeout = 10)
    {
        List<Task<IEnumerable<RiskDetails>>> tasks = new List<Task<IEnumerable<RiskDetails>>>();
        var responseList = new List<RiskDetails>();

        using (IDbConnection conn = new SqlConnection(connectionString))
        {
            conn.Open();

            foreach (var account in linkedAccounts)
            {
                var enumResults = conn.QueryAsync<RiskDetails>(storedProcedure,
                        new { UserID = account.UserID, CasinoID = account.CasinoID, GamingServerID = account.GamingServerID, AccountNo = account.AccountNumber, Group = account.GroupCode, EmailAddress = account.USEMAIL },
                        commandType: CommandType.StoredProcedure, commandTimeout: 0);

                //add task
                tasks.Add(enumResults);
            }

            //await and get results
            var results = await Task.WhenAll(tasks);
            foreach (var value in results)
                foreach (var riskDetail in value)
                    responseList.Add(riskDetail);
        }

        return responseList;
    }

我对@9​​87654322@如何执行的理解如下:

  • 为帐户 #1 执行查询
  • 等待查询 #1 的结果
  • 接收查询 #1 的结果
  • 对帐户 #2 执行查询
  • 等待查询 #2 的结果

我对@9​​87654323@如何执行的理解如下:

  • 将所有任务添加到 IEnumerable 实例中
  • 调用Task.WhenAll,它将开始执行对帐户#n 的查询
  • 查询相对于 SQL 服务器并行执行
  • Task.WhenAll 在所有查询执行时返回

据我了解,ExecuteSqlStoredProcedureSelectParallel 这个功能在时间上应该会有一点改进,但​​目前没有。

我对此的理解有误吗?

【问题讨论】:

  • 如果您在catch 块中抛出完全相同的异常,那么捕获异常有什么意义?
  • @FCin 这与我的问题有什么关系?您是否能够查看其余应用程序代码以了解异常处理的方式/位置?
  • 并行运行操作会产生开销,因此有时会抵消甚至超过所获得的任何收益。如果您的操作非常快,那么您可能看不到任何收益。如果每个操作需要 1 秒,但开销是 1 秒,那么串行或并行需要 2 秒。
  • 我想知道并行在连接级别是如何工作的。单个连接不是线程安全的。如果您得到正确的结果,我怀疑您的查询正在排队,并且执行结果应该是相同的。
  • @monstertjie_za:你说得对,这与你提出的问题无关,但这是一个不必要的敌对回答。 catch { throw; } 在所有情况下都是毫无意义的; 不管其他层如何处理异常。简单地完全删除 try/catch 并不会改变任何事情,因为没有必要捕获您无法处理的异常。

标签: c# sql .net asynchronous async-await


【解决方案1】:

你对ExecuteSqlStoredProcedureSelectParalel的理解并不完全正确。

调用 Task.WhenAll,它将开始为 Account #n 执行查询

Task.WhenAll 不会启动任何东西。在QueryAsync 方法返回后 - 任务已经启动并且正在运行甚至完成。当控制到达Task.WhenAll - 所有任务都已经开始。

查询相对于 SQL 服务器并行执行

这是一个复杂的主题。为了能够同时在同一个 sql 连接上执行多个查询 - 您在连接字符串中启用了 MultipleActiveResultSets 选项,否则将无法工作(抛出异常)。

然后,在许多地方,包括documentation,您可以读到 MARS不是关于并行执行的。它是关于语句交​​错的,这意味着 SQL Server 可能会在通过同一连接执行的不同语句之间切换,就像操作系统可能会在线程之间切换(在单核上)。引用以上链接:

MARS 操作在服务器上同步执行。陈述 SELECT 和 BULK INSERT 语句的交错是允许的。然而, 数据操作语言(DML)和数据定义语言(DDL) 语句以原子方式执行。任何试图执行的语句 在执行原子批处理时被阻塞。 并行执行 服务器不是 MARS 功能

现在,即使您的选择查询在服务器上并行执行,如果这些查询执行速度很快,那么在“性能”方面对您没有多大帮助。

假设您查询 10 个帐户,每个查询执行需要 1 毫秒(很正常,我会说预期的情况)。但是,每个查询都返回 100 行。现在,这 100 行应该通过网络传递给调用者。这是最昂贵的部分,与此相比,执行时间可以忽略不计(在这个特定示例中)。无论您是否使用 MARS - 您都只有一个与 sql server 的物理连接。即使您的 10 个查询在服务器上并行执行(由于上述原因,我对此表示怀疑) - 它们的结果也无法并行传递给您,因为您有一个物理连接。所以 10*100 = 1000 行,在这两种情况下,都是“按顺序”交付给您的。

从中应该清楚,您不应该期望您的Parallel 版本执行得更快。如果您希望它真正并行 - 为每个命令使用 separate 连接。

我还想补充一点,在这种情况下,您机器上的物理内核数量对性能没有不可忽略的影响。异步 IO 与阻塞线程无关,您可以在互联网上的许多地方阅读。

【讨论】:

  • 我理解并同意您在这里提到的内容。我注意到我的单一连接可能不像我想象的那么好。我正在稍微改变我的方法,为每个查询提供它自己的连接
  • 这可能会导致 SQL Server 争用。
【解决方案2】:

嗯,你的理解是正确的,但你需要了解底层核心,你机器拥有的物理核心数量。

您可以在给定时间创建多个任务,但这并不意味着所有任务都并行运行,每个任务代表线程并在物理核心上调度,而一个核心一次运行一个线程。

因此,如果您的机器拥有 4 个核心并且您创建了 8 个线程,那么您的机器将只运行 4 个线程,当线程调度在核心上时,其他线程将轮流运行,以防运行线程被阻塞或处于等待状态或完成。

上面我的意思是说,当您执行并行代码时,您还应该考虑您机器上的物理内核数量。这可能是您编码没有利用您已经完成的并行编码的原因之一。

如果内核数量少于任务/线程数量,那么还有太多的上下文切换会减慢您的程序速度。

除此之外,后台的任务并行库利用了线程池,线程池中的线程推荐用于小型操作。因为长时间运行的操作可能会消耗您的线程池,然后您的短期运行操作必须等待线程完成,这也会减慢您的应用程序的速度。因此,建议使用TaskCreationOptions.LongRunning 创建任务或使用async/await,这样您的线程池线程就不会为长时间运行的操作(数据库操作、文件读/写操作或外部 web/webservcie 调用以获取数据)获取 cosume。


除了上面的代码,

 var results = await Task.WhenAll(tasks);

这意味着等待所有任务执行完成,这意味着如果您有 5 个任务,其中 3 个已完成,但其中 2 个需要更长的时间才能完成,那么您的代码将等待该 2 个长时间运行的任务完成在执行下一行之前。


也检查一下:can a single SQL Server connection be shared among tasks executed in parallel

一个 SQLServer 连接可以由多个并行执行的任务共享,例如C# 程序中的线程或应用服务器中的请求。但大多数使用场景都需要您同步对 Connection 的访问。如果另一个任务正在使用它,则该任务将不得不等待连接。当您构建一个不会破坏或成为并行任务性能限制的共享连接机制时,您可能已经构建了一个连接池。

【讨论】:

  • 任务不以任何方式或形式表示线程。任务代表承诺或未来。 blog.stephencleary.com/2013/11/there-is-no-thread.html
  • @PauloMorgado - 你可以在这里查看:albahari.com/threading/#_Entering_the_Thread_Pool_via_TPL,它说任务使用线程池和 intrun 使用线程..
  • 那篇文章是关于使用 TPL 在线程池上调度工作的。这并不意味着所有任务都是线程池线程。你读过史蒂夫的博文吗?
  • @PauloMorgado - 好的,如果不是线程,你能告诉我什么是任务
  • 再说一遍,你读过 Stephen Cleary 的博文吗?都在那里。
猜你喜欢
  • 2022-11-15
  • 2017-03-04
  • 1970-01-01
  • 2012-11-06
  • 2018-01-11
  • 2020-07-12
  • 1970-01-01
  • 1970-01-01
  • 2020-12-31
相关资源
最近更新 更多