Async vs Parallel.Invoke vs Task.WhenAll 用于获取数据的 EntityF6 查询,在 ASP MVC 5 web 应用程序中

问题描述

我正在尝试找出除同步编程之外的最佳方法,用于执行一些检索数据的 EF6 查询。我将在这里发布所有 5 种方法(这些发生在控制器操作中):

//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 个不同的线程来处理这些任务
  2. Task.Run() 实际上就像 Invoke.Parallel 一样,将工作分散到 2 个不同的 cpu 上,使它们并行运行
  3. 先启动它们然后等待(C.1 示例)不应该使它们以某种并行方式运行吗?
  4. 完全不使用异步或并行会更好吗?

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

解决方法

async 代码可以通过在没有 await 的情况下调用,然后等待 Task.WaitAll() 来与并行性混合。但是,查看并行性时的主要考虑因素是确保调用的代码是线程安全的。 DbContexts 不是线程安全的,因此要运行并行操作,您需要为每个方法使用单独的 DbContext 实例。这意味着通常依赖依赖注入来接收 DbContext/工作单元并获得生命周期范围为 Web 请求之类的引用的代码不能在并行调用中使用。并行化的调用将需要一个 DbContext,该 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();

作为一个非常基本的示例,其中存储库类获取 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 等。因此,这有时会导致间歇性错误,这些错误可能不会在测试期间出现,或者在没有负载时可靠地出现(查询都很快完成并且不会相互绊倒),但在负载下运行时会因例外情况而停止。为了解决这个问题,需要为每个方法限定 Repository 中的 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 Factory 类的东西来创建可以模拟的 DbContext。关键是必须将 DbContext 的范围移动到并行化方法内。 AsNoTracking() 很重要,因为我们不能让这个 DbContext “跟踪”这个订单;当我们想要保存订单和任何其他关联实体时,我们必须将此订单与新的 DbContext 实例相关联。 (这个正在被处理)如果实体仍然认为它被跟踪,那将导致错误。这也意味着存储库 Save 必须更改为更像:

Repository.Save(order);

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

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

async/await 是一种很好的模式,适用于较长时间的个别操作,其中网络请求可能需要等待几秒钟,例如搜索或报告。这释放了 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。报告或建筑物导出等