个人等待与Task.WhenAll

时间:2018-05-28 06:34:10

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

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

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

我对ExecuteSqlStoredProcedureSelect执行方式的理解如下:

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

我对ExecuteSqlStoredProcedureSelectParalel执行方式的理解如下:

  • 将所有任务添加到IEnumberable实例
  • 调用Task.WhenAll,它将开始执行帐户#n
  • 的查询
  • 查询与SQL Server相对并行执行
  • 当执行所有查询时,Task.WhenAll返回

根据我的理解,ExecuteSqlStoredProcedureSelectParalel关于时间,这个函数应该有一点改进,但​​目前没有。

我对此的理解是错误的吗?

2 个答案:

答案 0 :(得分:4)

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

  

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

的查询

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

  

查询与SQL Server相对并行执行

这是一个复杂的主题。为了能够同时在同一个sql连接上执行多个查询 - 你的连接字符串中启用了MultipleActiveResultSets选项,如果没有这个选项就行不通(抛出异常)。

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

  

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

现在,即使您的选择查询在服务器上并行执行,如果这些查询执行速度很快,那么在“性能”方面也不会给您带来太多帮助。

假设您查询了10个帐户,并且每个查询执行需要1毫秒(非常正常,我说预期情况)。但是,每个查询返回100行。现在,这100行应该通过网络传递给调用者。这是成本最高的部分,执行时间与此相比可忽略不计(在此具体示例中)。无论您是否使用MARS - 您只有一个与sql server的物理连接。即使您的10个查询在服务器上并行执行(我怀疑上述情况) - 它们的结果也无法并行传递给您,因为您有一个物理连接。因此,在这两种情况下,10 * 100 = 1000行都会按顺序发送给您#34;

由此可以清楚地表明,您不应期望Parallel版本的执行速度明显加快。如果您希望它真正并行 - 请为每个命令使用单独的连接。

我还要补充说,在这种情况下,机器上的物理内核数量对性能没有不可忽视的影响。异步IO不是关于阻塞线程,你可能会通过互联网在很多地方阅读。

答案 1 :(得分:3)

嗯,您的理解是正确的,但您需要了解底层核心,您加工的物理核心数量。

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

因此,如果您的机器已经让4核心并且您创建了8个线程,那么您将只运行4个线程,其他线程将在核心上运行线程被阻止或处于等待状态或已完成时转向。

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

如果核心数量少于任务/线程数量,那么还会有更多的事情,那么当你的程序速度变慢时也会有太多的上下文切换。

除此之外,引擎盖下的Task并行库使用了Threadpool,并且线程池中的线程建议用于小型操作。因为长时间运行操作可能会占用您的线程池,然后您的短期运行操作必须等待线程完成,这也会降低您的应用程序速度。因此建议使用TaskCreationOptions.LongRunning创建任务或使用async/await,这样线程池线程就不会获得长期运行的cosume(数据库操作,文件读/写操作或外部web / webservcie调用来获取数据) )。

除了上面的代码,

 var results = await Task.WhenAll(tasks);

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

另请检查:can a single SQL Server connection be shared among tasks executed in parallel

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