【问题标题】:The correct way to await inside a foreach loop在 foreach 循环中等待的正确方法
【发布时间】:2020-01-30 11:03:00
【问题描述】:

这是使用 async 使用 foreach 循环的正确方法吗?有没有更好更有效的方法? IAsyncEnumerable? (忽略两个表可以连接在一起的事实,这是一个简单的例子)

public async Task<IList<ContactAndAccounts>> GetAll()
{
    var accounts = await _dbContext.Account.Where(x => x.Name == "Amazing").ToListAsync();

    foreach(var account in accounts)
    {
           accounts.Contacts = await GetContact(account.Id);
    }

    return accounts;
}

public async Task<IList<contact>> GetContact(Guid id)
{
    return await _dbContext.Contact.Where(x => x.AccountLinkId = id).ToListAsync();
}

【问题讨论】:

  • 无论如何,您都不应该在单独的查询中获取联系人。你为什么不Include他们?
  • 这里的问题与await和loops无关。这是缺失的关系。解决方法是使用Include(acc=&gt;acc.Contacts)Account 应该有一个 Contacts 属性,并且 DbContext 应该包含一对多关系
  • 使用正确的 dbcontext,查询变为 _dbContext.Account.Include(acc=&gt;acc.Contatcts).Where(...).ToListAsync()。不需要循环
  • @PanagiotisKanavos 我同意,但请阅读原始问题“忽略两个表可以连接在一起的事实,这是一个简单的示例”
  • 这是主要问题,不容忽视。其他一切都是创可贴。这不会解决真正的问题。正确的修复比此处发布的每个解决方案都更容易

标签: c# async-await c#-8.0 ef-core-3.0 iasyncenumerable


【解决方案1】:

我同意Johnathan Barclay's answer,但我会说从数据库的角度来看,您可能会发现执行单个查询大查询比执行大量小查询要快。

执行一个查询并传入所有 ID 通常比多个单独查询便宜。

public async Task<IList<ContactAndAccounts>> GetAll()
{
    var accounts = await _dbContext.Account.Where(x => x.Name == "Amazing").ToListAsync();

    var contacts = await GetContacts(accounts.Select(o => o.Id));

    // Map contacts to accounts in memory

    return accounts;
}

public async Task<IList<contact>> GetContacts(List<Guid> ids)
{
    return await _dbContext.Contact.Where(x => ids.Contain(x.AccountLinkId)).ToListAsync();
}

【讨论】:

  • 是的,我专注于foreach 循环并没有注意到这一点。 +1
  • 真正的解决方案是在AccountContact 之间添加一个关系并使用Include(acc=&gt;acc.Contacts)
  • @PanagiotisKanavos 如果不存在导航属性,则缺少此 OP 可以进行连接。但我总是发现它们在 Linq 中最终看起来相当混乱。
  • 这就是为什么必须添加关系。其他一切都是创可贴。所有答案中的代码已经比正确的 LINQ 查询复杂得多
  • 如果你看一下这个GitHub thread,你会发现目前集合导航属性和GroupJoins 支持的内容有很大的不同(虽然从技术上讲它们应该是等价的) .这使得导航属性的使用几乎是强制性的。
【解决方案2】:

这绝对不是最有效的方法。

每个await 都会导致循环暂停,直到Task 完成。

您希望所有任务同时运行:

public async Task<IList<ContactAndAccounts>> GetAll()
{
    var accounts = await _dbContext.Account.Where(x => x.Name == "Amazing").ToListAsync();

    await Task.WhenAll(accounts.Select(async account => 
    {
        accounts.Contact = await GetContact(account.Id);
    }));

    return accounts;
}

Select 将为每个项目生成一个Task,可以通过Task.WhenAll 等待。

【讨论】:

  • Yes WhenAll 是要走的路。恭喜你比我快:)
  • 当只需要 1 个查询时,这仍然会执行 101 个查询。并行运行 100 个查询不会提高性能,它会增加锁争用
  • @PanagiotisKanavos 是的,我现在注意到了,但我回答了手头的问题。迈克尔已经在他的回答中详细说明了这一点。
【解决方案3】:

有没有更好更有效的方法? IAsyncEnumerable?

是的!更改签名并将accounts.Contacts = await GetContact(account.Id); 替换为yield return await GetContract(account.Id)

【讨论】:

  • 这不是更有效。根本问题是相关对象是使用不同的查询一个一个地加载的,而不是一次加载所有对象的单个查询。 IAsyncEnumerable 无法解决这个问题
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-05-09
  • 2018-02-12
  • 1970-01-01
  • 1970-01-01
  • 2015-07-04
  • 2019-05-22
相关资源
最近更新 更多