个人等待 vs Task.WhenAll

3
我有以下两种方法,它们产生相同的结果。
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;
    }

我对ExecuteSqlStoredProcedureSelect执行的理解如下:

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

我对ExecuteSqlStoredProcedureSelectParallel执行的理解如下:

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

据我了解,ExecuteSqlStoredProcedureSelectParallel在时间方面应该有一些改进,但目前还没有。

我的理解是错误的吗?


3
如果在 catch 块中抛出完全相同的异常,那么捕获异常有什么意义呢? - FCin
1
我想知道并行处理在连接层面上是如何运作的。单个连接并不是线程安全的。如果您正在获得正确的结果,我怀疑有些东西正在排队等待您的查询,并且执行应该最终相同。 - bommelding
1
@monstertjie_za:你说得没错,这与你所问的问题无关,但那是一种不必要的敌对回复。catch {throw;}在所有情况下都是毫无意义的;无论其他层如何处理异常。既然没有处理异常的意义,那么完全可以直接删除try/catch,不会有任何变化。 - Flater
但目前还没有。你是如何计时的? - mjwills
顺便提一下,建议将返回类型从Task<IEnumerable<RiskDetails>>更改为Task<IList<RiskDetails>>,以明确表明任务的结果是一个实体集合,而不是一个延迟枚举。 - Theodor Zoulias
显示剩余3条评论
2个回答

5
你对ExecuteSqlStoredProcedureSelectParalel的理解并不完全正确。
调用Task.WhenAll,将开始执行帐户#n的查询。 Task.WhenAll不会启动任何东西。在QueryAsync方法返回后-任务已经开始运行或甚至已经完成。当控制到达Task.WhenAll时-所有任务都已经开始了。
查询相对于SQL服务器是并行执行的。
这是一个复杂的主题。要能够同时在同一SQL连接上执行多个查询,您必须在连接字符串中启用MultipleActiveResultSets选项,否则将无法工作(抛出异常)。
然后,在许多地方,包括文档中,您可以阅读到MARS与并行执行无关。它是关于语句交错的,这意味着SQL Server可能在同一连接上切换执行不同的语句,就像操作系统可能在单个内核上切换线程一样。来自上面链接的引用:
"MARS操作在服务器上同步执行。允许选择和BULK INSERT语句的语句交错。但是,数据操作语言(DML)和数据定义语言(DDL)语句以原子方式执行。任何尝试在原子批处理执行时执行的语句都将被阻止。服务器上的并行执行不是MARS功能。"
现在,即使您的选择查询在服务器上并行执行,如果这些查询的执行速度很快,那也不会对"性能"有太大帮助。
假设您查询10个帐户,每个查询执行时间为1ms(相当正常,我会说是预期情况)。但是,每个查询返回100行。现在,这些100行应该通过网络传递给呼叫者。这是最昂贵的部分,执行时间与此相比微不足道(在这种特定的示例中)。无论您是否使用MARS-都只有一个物理连接与SQL服务器。即使您的10个查询在服务器上并行执行(由于上述原因,我怀疑)-它们的结果也不能并行地传递给您,因为您只有一个物理连接。因此,在两种情况下都以"顺序"的方式向您传递了10 * 100 = 1000行。
从中可以清楚地看出,您不应该指望您的Parallel版本执行得更快。如果您希望它真正并行,请为每个命令使用单独的连接。
我还想补充一点,在这种情况下,您的计算机上的物理核心数量对性能没有任何重要影响。异步IO与阻塞线程无关,您可能在互联网上的许多地方都可以读到这一点。

我理解并同意你在这里提到的内容。我注意到我的单一连接可能不是我认为的那么好的选择。我正在稍微改变我的方法,为每个查询提供自己的连接。 - monstertjie_za
这可能会导致 SQL Server 上的争用。 - Paulo Morgado

3
好的,您的理解是正确的,但是您需要了解底层核心,即您的机器有多少物理核心。
您可以在给定时间内创建多个任务,但这并不意味着所有任务都在并行运行,每个任务代表一个线程,并在物理核心上进行调度,反过来,一个核心一次只能运行一个线程。
因此,如果您的机器有4个核心,并且您创建了8个线程,则您的机器仅运行4个线程,其他线程将在线程被调度到核心时获得轮换,在运行线程被阻塞或处于等待状态或已完成的情况下。
以上我想说的是,当您进行并行编码时,您还应该考虑您的机器上拥有的物理核心数量。这可能是您的代码没有利用您所做的并行编码的优势的原因之一。
还有一件事,如果核心数少于任务/线程数,则会有太多的上下文切换,这可能会减慢程序的速度。
除了以上内容,在您的代码中,任务并行库在幕后使用线程池,并且建议使用线程池中的线程进行小型操作。因为长时间运行的操作可能会消耗您的线程池,然后您的短时间运行的操作必须等待线程结束,这也会减慢您的应用程序。因此,建议使用TaskCreationOptions.LongRunning创建任务或使用async/await,以便您的线程池线程不会被长时间运行的操作(数据库操作、文件读写操作或外部Web/ Web服务调用以获取数据)占用。
除了以上内容,在您的代码中,
 var results = await Task.WhenAll(tasks);

这意味着等待所有任务执行完成,也就是说,如果你有5个任务,其中3个完成了,但还有2个正在进行中,那么你的代码将在执行下一行之前等待这2个长时间运行的任务完成。

也可以查看此内容:单个SQL Server连接可在并行执行的任务之间共享吗

一个SQLServer连接可以被多个任务并行执行所共享,例如C#程序中的线程或应用服务器中的请求。但大多数使用情况都需要同步访问连接。如果另一个任务正在使用连接,则当前任务必须等待连接。到你构建一个共享连接机制,以便不会破坏或成为并行任务性能的限制时,你可能已经构建了一个连接池。


1
任务并不以任何方式代表线程。任务代表承诺或未来。https://blog.stephencleary.com/2013/11/there-is-no-thread.html - Paulo Morgado
@PauloMorgado - 你可以在这里查看:http://www.albahari.com/threading/#_Entering_the_Thread_Pool_via_TPL ,它说任务使用线程池,而intrun使用线程。 - Pranay Rana
那篇文章是关于使用TPL在线程池上安排工作的。这并不意味着所有任务都是线程池线程。你读过Steve的博客文章吗? - Paulo Morgado
@PauloMorgado - 好的,那么你能告诉我任务是什么吗?如果它不是线程的话。 - Pranay Rana
@PauloMorgado - 如果我理解正确,那么这篇文章说的是你的IO操作在操作系统级别上得到执行,而启动操作的线程会等待操作完成,如果这是正确的,那么在C#中要达到await,你需要一个线程对吧? - Pranay Rana
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接