在 ASP MVC 5 Web 应用程序中获取数据的 EntityF6 查询的异步 vs Parallel.Invoke vs Task.WhenAll

Async vs Parallel.Invoke vs Task.WhenAll for EntityF6 query that gets data, in ASP MVC 5 web app

我想弄清楚除了同步编程之外,执行一些检索数据的 EF6 查询的最佳方法是什么。我将在此处 post 所有 5 种方法(这些方法发生在 Controller Action 中):

//would it be better to not "async" the ActionResult?
public async Task<ActionResult> Index{
   // I depend on this so I don't even know if it's ok to make it async or not -> what do you think?
   var userinfo = _dataservice.getUserInfo("John");

   // C1: synchronous way
   var watch1 =  System.Diagnostics.Stopwatch.StartNew();
   var info1 = _getInfoService.GetSomeInfo1(userinfo);
   var info2 = _getInfoService.GetSomeInfo2(userinfo);
   watch1.Stop();
   var t1 = watch.EllapsedMilliSeconds; // this takes about 3200
   
   // C2: asynchronous way
   var watch2 =  System.Diagnostics.Stopwatch.StartNew();
   var infoA1 = await _getInfoService.GetSomeInfoAsync1(userinfo).ConfigureAwait(false);
   var infoA2 = await _getInfoService.GetSomeInfoAsync2(userinfo).ConfigureAwait(false);
   watch2.Stop();
   var t2 = watch2.EllapsedMilliSeconds; // this takes about 3020

   // C2.1: asynchronous way launch then await
   var watch21 =  System.Diagnostics.Stopwatch.StartNew();
   var infoA21 = _getInfoService.GetSomeInfoAsync1(userinfo).ConfigureAwait(false);
   var infoA22 = _getInfoService.GetSomeInfoAsync2(userinfo).ConfigureAwait(false);
   // I tought if I launch them first then await, it would run faster...but not
   var a = await infoA21;
   var b = await infoA22;
   watch21.Stop();
   var t21 = watch21.EllapsedMilliSeconds; // this takes about the same 30201

   // C3: asynchronous with Task.Run() and await.WhenAll()
   var watch1 =  System.Diagnostics.Stopwatch.StartNew();
   var infoT1 = TaskRun(() => _getInfoService.GetSomeInfo1(userinfo));
   var infoT2 = TaskRun(() => _getInfoService.GetSomeInfo2(userinfo));
await Task.WhenAll(infoT1,infoT2)
   watch3.Stop();
   var t3 = watch3.EllapsedMilliSeconds; // this takes about 2010

   // C4: Parallel way
   MyType var1; MyType2 var2;
   var watch4 =  System.Diagnostics.Stopwatch.StartNew();
   Parallel.Invoke(
      () => var1 = _getInfoService.GetSomeInfoAsync1(userinfo).GetAwaiter().GetResult(),// also using just _getInfoService.GetSomeInfo1(userinfo) - but sometimes throws an Entity error on F10 debugging
      () => var2 = _getInfoService.GetSomeInfoAsync2(userinfo).GetAwaiter().GetResult()// also using just _getInfoService.GetSomeInfo2(userinfo)- but sometimes throws an Entity error on F10 debugging
   );
   watch4.Stop();
   var t4 = watch4.EllapsedMilliSeconds; // this takes about 2012
}

方法实现:

public MyType1 GetSomeInfo1(SomeOtherType param){
 // result = some LINQ queries here
 Thread.Sleep(1000);
 return result;
}
public MyType2 GetSomeInfo2(SomeOtherType param){
 // result = some LINQ queries here
 Thread.Sleep(2000);
 return result;
}

public Task<MyType1> GetSomeInfoAsync1(SomeOtherType param){
 // result = some LINQ queries here
 Thread.Sleep(1000);
 return Task.FromResult(result);
}

public Task<MyType2> GetSomeInfoAsync2(SomeOtherType param){
 // result = some LINQ queries here
 Thread.Sleep(2000);
 return Task.FromResult(result);
}
  1. 如果我理解正确,await 2 个任务(如在 C2 和 C2.1 中)不会使它们 运行 并行(甚至在我启动它们的 C.1 示例中也不行)首先然后等待),它只是释放当前线程并将它们交给另外两个将处理这些任务的不同线程
  2. Task.Run() 实际上会像 Invoke.Parallel 那样做,将工作分散在 2 个不同的 CPU 上,使它们 运行 并行
  3. 先启动它们然后等待(C.1 示例)不应该使它们 运行 成为某种并行方式?
  4. 根本不使用异步或并行会更好吗?

请让我了解这些示例如何才能获得异步和更好的性能,以及是否对 EntityF 有任何我必须考虑的影响。我已经阅读了几天,我只会感到困惑,所以请不要再给我阅读链接:)

async 代码可以通过在没有 await 的情况下调用并等待 Task.WaitAll() 来与并行混合。但是,查看并行性时的主要考虑因素是确保调用的代码是线程安全的。 DbContexts 是 not 线程安全的,因此对于 运行 并行操作,您需要为每个方法单独的 DbContext 实例。这意味着通常依赖于依赖注入来接收 DbContext/Unit Work 并会获得生命周期范围内类似于 Web 请求的引用的代码 不能 用于并行化电话。并行化的调用需要有一个 DbContext 范围仅限于该调用。

在处理与 EF 实体一起使用的并行化方法时,这也意味着您需要确保任何实体引用都被视为分离的实体。它们不能安全地相互关联,就好像它们是由不同并行任务中的不同 DbContext 返回的一样。

例如,使用正常 async & await:

var order = await Repository.GetOrderById(orderId);
var orderLine = await Repository.CreateOrderLineForProduct(productId, quantity);
order.OrderLines.Add(orderLine);
await Repository.SaveChanges();

作为一个非常基本的示例,其中存储库 class 注入了 DbContext。 CreateOrderLine 方法将使用 DbContext 来加载 Product 和其他可能的细节来创建 OrderLine。等待时,async 变体确保一次只有一个线程访问 DbContext,因此存储库可以使用同一个 DbContext 实例。 Order、新 OrderLine、Product 等都由同一个 DbContext 实例跟踪,因此存储库针对该单个实例发出的 SaveChanges 调用将按预期工作。

如果我们尝试像这样并行化它:

var orderTask = Repository.GetOrderById(orderId);
var orderLineTask = Repository.CreateOrderLineForProduct(productId, quantity);
await Task.WhenAll(orderTask, orderLineTask);
var order = orderTask.Result;
var orderLine = orderLineTask.Result;

order.OrderLines.Add(orderLine);
await Repository.SaveChanges();

这可能会导致 EF 出现异常,即 DbContext 作为 GetOrderById 跨线程访问,并在 CreateOrderLine 中调用。更糟糕的是,EF 不会检测到它正在被多个线程调用 ,直到 这些线程都尝试同时访问 DbSet 等。因此,这有时会导致间歇性错误,该错误可能不会在测试期间出现,或者在没有负载时可靠地出现(所有查询都很快完成并且不会相互绊倒)但是在 运行 时异常停止负载下宁。为了解决这个问题,存储库中的 DbContext 引用需要为每个方法限定范围。这意味着不是使用注入的 DbContext,它需要看起来更像:

public Order GetOrderById(int orderId)
{
    using(var context = new AppDbContext())
    {
        return context.Orders
            .Include(x=>x.OrderLines)
            .AsNoTracking()
            .Single(x => x.OrderId == orderId);
    }
}

我们仍然可以使用依赖注入来注入类似 DbContext 工厂的东西 class 来创建可以模拟的 DbContext。关键是 DbContext 的范围必须移动到并行化方法中。 AsNoTracking() 很重要,因为我们不能让此 DbContext“跟踪”此订单;当我们想要保存订单和任何其他关联实体时,我们必须将此订单与新的 DbContext 实例相关联。 (这个正在处理)如果实体仍然认为它被跟踪,那将导致错误。这也意味着存储库 Save 必须更改为更像:

Repository.Save(order);

传入一个实体,将它和所有引用的实体关联到一个 DbContext,然后调用 SaveChanges.

不用说这开始变得混乱了,它甚至还没有涉及异常处理之类的事情。由于需要使用分离的实体,您还会失去更改跟踪等方面。为了避免跟踪和未跟踪实体之间的潜在问题,我建议并行化代码应始终处理 POCO 视图模型或对实体进行更完整的“操作”,而不是做返回分离实体之类的事情。我们希望避免将可能通过跟踪的订单(使用同步或异步调用)调用的代码与未跟踪的订单(因为它是并行调用的结果)调用的代码混淆。也就是说,它可以有它的用途,但我强烈建议将它的使用保持在最低限度。

async/await 可以是一个很好的模式,可以用于较长的、单独的操作,在这些操作中,Web 请求可能需要等待几秒钟,例如搜索或报告。这释放了 Web 请求处理线程,以便在用户等待时开始响应其他请求。因此,它用于提高服务器 响应能力 ,不要与加快调用速度相混淆。对于简短而快速的操作,它最终会增加一些额外的开销,因此这些应该只保留为同步调用。 async 不是我认为在应用程序中需要“全有或全无”决定的东西。

所以在上面的示例中,按 ID 加载订单并创建订单行是我通常会保持同步而不是异步的事情。按 ID 加载实体图通常非常快。我将利用 async 的一个更好的例子是:

var query =  Repository.GetOrders()
    .Where(x =>  x.OrderStatus.OrerStatusId == OrderStatus.New 
        && x.DispatchDate <= DateTime.Today());
if (searchCriteria.Any())
    query = query.Where(buildCriteria(searchCriteria));

var pendingOrders = await query.Skip(pageNumber * pageSize)
    .Take(PageSize)
    .ProjectTo<OrderSearchResultViewModel>()
    .ToListAsync();

在这个例子中,我有一个搜索操作,预计 运行 可能会涉及大量订单,并且在获取结果页面之前可能包括效率较低的用户定义的搜索条件。 运行 可能需要不到一秒或几秒的时间,并且当时可能有许多呼叫(包括其他搜索)需要处理来自其他用户的呼叫。

并行化更适用于混合需要作为一个单元完成的长操作和短操作的情况 运行,因此一个人不需要等待另一个人完成开始。在使用 EF 实体进行操作时,需要在此模型中多加注意,因此我绝对不会将其设计为系统中的“默认”模式。

总结一下:

同步 - 快速访问数据库或内存缓存,例如按 ID 提取行或一般查询预计在 250 毫秒或更短的时间内执行。 (基本上默认)

异步 - 跨较大集合的较大查询,执行时间可能较慢,例如动态搜索,或预计会被极其频繁调用的较短操作。

并行 - 将启动多个查询以完成的昂贵操作,其中可以“剥离”必要数据的查询,并且 运行 完全独立地在后台完成。 IE。报告或建筑出口等