【问题标题】:Optimizing async/await - assigning variable in forEach优化 async/await - 在 forEach 中分配变量
【发布时间】:2021-06-28 13:59:18
【问题描述】:

我知道在foreach 中使用await 由于性能原因并不是一个好的做法,因为它会按顺序等待每个任务。

foreach (var task in result)
{
     task.Stages = await GetStagesForTask(task.Id);
}

那么我该如何改进该代码呢?我试图做这样的事情:

List<Task> listOfTasks = new List<Task>();

foreach (var task in result)
{
    var stage = GetStagesForTask(task.Id);
    listOfTasks.Add(stage);
    task.Stages = stage;
}

await Task.WhenAll(listOfTasks);

但当然因为task.Stages = stage;这里的类型不正确,它不会起作用。

【问题讨论】:

  • 在这一行:foreach (var task in result)task 变量的类型是什么?
  • 你怎么知道在foreach 中使用await 不是一个好习惯? AFAIK 这是一种完全有效的方法。它很简单,它没有引入并发/线程安全考虑,并且在许多情况下(例如访问数据库/文件系统)它具有与并发方法相当的性能。
  • 为什么你会说“我知道在 foreach 中使用 await 不是一个好习惯,因为性能原因”?那是从哪里来的?这两种方法最适合视情况而定,例如:如果您发出 multilpe Http 请求,则使用 Task.WaitAll 很好,如果您在 Task 内部执行任何类型的数据库操作,则不建议使用任何“ lock”,这与在循环中使用 await 基本相同,许多 dbs 不支持多个 cons 更新相同的行,或者文件被多个线程使用,这可能是一个大问题。更不用说内存消耗了
  • 好点@MestreDosMagros,但是例如,如果每个查询都是独立的,那么等待每个结果都是资源浪费。所以 tl;dr 如果操作是独立的,最好同时进行。

标签: c# .net async-await


【解决方案1】:

您可以将 LINQ 与异步委托一起使用:

var tasks = result.Select(async task =>
{
    var stage = await GetStagesForTask(task.Id);
    task.Stages = stage;
});

await Task.WhenAll(tasks);

或者引入一个局部函数:

List<Task> listOfTasks = new List<Task>();

async Task SetStagesAsync(YourTask task)
{
    task.Stages = await GetStagesForTask(task.Id);
}

foreach (var task in result)
{
    listOfTasks.Add(SetStagesAsync(task));
}

await Task.WhenAll(listOfTasks);

甚至是两者的结合:

async Task SetStagesAsync(YourTask task)
{
    task.Stages = await GetStagesForTask(task.Id);
}

await Task.WhenAll(result.Select(SetStagesAsync));

【讨论】:

  • 你能解释一下为什么这与foreach 的工作方式不同,即使await 在选择循环中?
  • 假设GetStagesForTask 是真正异步的(即释放线程),async lambda 将返回一个不完整的Task。这允许后续迭代发生,而无需等待前一个迭代完成。
【解决方案2】:

如果您不介意同时改变实体的 Stages 属性,Johnathan Barclay 的 solution 是完美的。但是,如果您希望将突变推迟到所有异步操作完成之前,那么您可以考虑将您的实体投影到Task&lt;Action&gt;s 的列表中,然后使用await 使用Task.WhenAll 执行这些任务,最后按顺序调用所有由此产生的Actions:

Task<Action>[] tasks = entities.Select(async entity =>
{
    var stages = await GetStagesForEntityAsync(entity.Id);
    return new Action(() => entity.Stages = stages);
}).ToArray();

Action[] actions = await Task.WhenAll(tasks);

foreach (var action in actions) action.Invoke();

在上面的示例中,我已将示例中的 taskresult 变量重命名为 entity/entities,以防止实体与内置 Task 类之间出现任何混淆。

LINQ Select 运算符可以轻松地将一个可枚举对象投影到另一个对象,当您想要从对象列表创建自定义任务列表时,它尤其方便。

【讨论】:

    【解决方案3】:

    解决此问题的一种巧妙方法是为每个任务创建一个线程,await 将发生在单独的线程内。因此,您将在foreach 中创建线程,它们将在单独的线程中执行等待,然后另一个foreach 将为每个线程调用.Join()。通过这种方式,您不会有顺序等待,而是创建线程,它们将并行工作,您将等待最长的任务,而不是所有任务所需时间的总和。

    但是,如果您有很多任务,请注意耗尽所有资源。如果你有很多任务,那么将它们分成大概 10 个线程的块并应用我上面描述的方法。

    【讨论】:

    • 这不能用Parallel.ForEach() 实现吗?
    • @StefanoCavion 是的,可以通过这些方式实现,你是对的。
    • @StefanoCavion Parallel 永远不应该与 async/await 一起使用。这是迁移到异步流期间引入的一个流行错误。异步委托返回 Task 并且 Parallel 构造不等待该任务。未观察到异常,并且在 Parallel.For 调用运行后您无法确定所有工作是否已实际完成。
    • 看看并行实现:public static ParallelLoopResult ForEach&lt;TSource&gt;(IEnumerable&lt;TSource&gt; source, Action&lt;TSource&gt; body); 所以有Action&lt;T&gt; 而不是Func&lt;Task&lt;T&gt;&gt;
    • @DiPix 感谢您的澄清。我仍然不明白为什么我应该做类似foreachawait 的事情,而不是做Task.Runawait 的结果你能给我一个提示吗?
    猜你喜欢
    • 1970-01-01
    • 2021-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-09
    • 1970-01-01
    • 2020-12-07
    • 1970-01-01
    相关资源
    最近更新 更多