【问题标题】:Parallel or async ASP.NET Core C#并行或异步 ASP.NET Core C#
【发布时间】:2018-02-27 09:17:38
【问题描述】:

我用谷歌搜索了很多,但恐怕我不完全理解并发和并行的后果。

我有大约 3000 行数据库对象,每个对象平均附加了 2-4 个逻辑数据,这些数据需要作为搜索查询的一部分进行验证,这意味着验证服务需要执行大约3*3000次。例如。用户已经过滤了颜色,然后每一行都需要验证颜色并返回结果。找到匹配项后循环不会中断,这意味着始终需要评估所有逻辑对象(这是由于计算相关性而不是匹配项)。

这是在用户选择各种属性时按需完成的,这意味着性能是这里的关键。

我目前正在使用 Parallel.ForEach 执行此操作,但想知道使用异步行为是否更聪明?

当前方式

var validatorService = new LogicalGroupValidatorService();
ConcurrentBag<StandardSearchResult> results = new ConcurrentBag<StandardSearchResult>();
Parallel.ForEach(searchGroups, (group) =>
{
    var searchGroupResult = validatorService.ValidateLogicGroupRecursivly(
        propertySearchQuery, group.StandardPropertyLogicalGroup);
    result.Add(new StandardSearchResult(searchGroupResult));
});

异步示例代码

var validatorService = new LogicalGroupValidatorService();
List<StandardSearchResult> results = new List<StandardSearchResult>();
var tasks = new List<Task<StandardPropertyLogicalGroupSearchResult>>();

foreach (var group in searchGroups)
{
    tasks.Add(validatorService.ValidateLogicGroupRecursivlyAsync(
        propertySearchQuery, group.StandardPropertyLogicalGroup));
}

await Task.WhenAll(tasks);

results = tasks.Select(logicalGroupResultTask => 
    new StandardSearchResult(logicalGroupResultTask.Result)).ToList();

【问题讨论】:

  • 由于您似乎实现了这两个版本,如果您测量两个版本的执行时间会是什么样子?
  • 衡量性能差异了吗?这是唯一确定的方法。也就是说,我的猜测是在这种情况下并行应该表现更好,因为异步主要是“在等待其他系统时不要阻塞主线程”
  • 那么 ValidateLogicGroupRecursivly 是与数据库一起工作还是一切都在内存中完成?
  • 对于 3000 或 9000 行,这无关紧要,除非您正在对数据库进行一些往返或每行的任何 CPU 繁重的工作都无关紧要。循环 9000 次迭代来计算一些简单的公式或值对于今天的计算机来说不算什么。除非您已经测量了现实世界的瓶颈问题,否则只需在单个线程上执行即可。在 ASP.NET Core 中启动/排队太多线程实际上可能会降低整体性能而不是提高整体性能(ASP.NET Core 在用完(排队)线程时在高流量情况下开始拒绝连接)
  • 如果您必须对数据库进行任何往返,可能有更好的方法来解决它(在代码中找出您需要的值,然后在一个查询中获取所有值,然后在本地执行计算) (重 CPU 意味着每行 1-2 毫秒,因此整个计算需要 9 到 18 秒。今天 CPU 上的典型计算在 ns 范围内,因此 9000 条记录如果需要 1 或 2 则没有太大区别毫秒)

标签: c# asynchronous asp.net-core parallel.foreach


【解决方案1】:

并行和异步的区别是这样的:

  • 并行:启动多个线程并将工作分配给每个线程
  • 异步:以非阻塞方式完成工作。

这是否有所作为取决于以异步方式阻塞的是什么。如果你在 CPU 上工作,是 CPU 阻塞了你,因此你仍然会得到多个线程。如果是 IO(或除 CPU 之外的其他任何东西,您将重用相同的线程)

对于您的特定示例,这意味着以下内容:

Parallel.ForEach => 为列表中的每个项目启动新线程(启动的线程数由 CLR 管理)并在不同的线程上执行每个项目

async/await => 做这部分工作,但让我继续执行。由于您有很多项目,这意味着要多次说。现在取决于结果:

  • 如果this bit of work在CPU上,效果是一样的
  • 否则,当工作在其他地方完成时,您将只使用一个线程

【讨论】:

  • 基于其他 cmets(开始 3000 个任务并不是最聪明的做法),在 Task.Run() 中包装标准 foreach(替换 Parallel.ForEach)会更好吗并等待该任务?如果我理解正确的话,这意味着每个 9000 次迭代请求只创建一个任务。
  • @Max:这样做没有任何好处。您只需释放请求线程并使用池中的另一个线程。因此,线程使用与在请求线程上执行相同,只是在切换线程时会产生一点线程池管理开销。在 ASP.NET Core 请求线程上做 CPU 绑定的工作非常好
  • @Tseng,所以本质上,它可以像:var results = searchGroups.Select(group =&gt; new StandardSearchResult(validatorService.ValidateLogicGroupRecursivly(propertySearchQuery, group.StandardPropertyLogicalGroup))).ToList(); 一样简单,然后就满意了吗?
  • @Max 这里的关键是衡量性能。这不仅意味着您为单个用户运行它时的性能,还意味着如果您需要同时为多个用户执行此操作会发生什么。对于一个用户,并行执行它很可能会更快,对于多个用户,您可能会得到不同的结果(取决于它的 CPU 负载程度)。
  • 这有点误导,因为 1) Parallel.ForEach 确实启动新线程,它使用线程池中的任务,以及 2) 调用 async/await on Task.WhenAll 不可能使用单个线程,相反,Task.WhenAll(tasks) 将始终创建 tasks.Count 任务。
猜你喜欢
  • 1970-01-01
  • 2023-03-19
  • 1970-01-01
  • 2018-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多