-1

我试图弄清楚除了同步编程之外,执行一些检索数据的 EF6 查询的最佳方法是什么。我将在这里发布所有 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 个不同的线程来处理这些任务
  2. Task.Run() 实际上会像 Invoke.Parallel 那样做,将工作分散到 2 个不同的 CPU 上,使它们并行运行
  3. 先启动它们然后等待(C.1 示例)不应该使它们以某种并行方式运行吗?
  4. 完全不使用异步或并行会更好吗?

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

4

1 回答 1

0

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

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

例如,使用普通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 来加载产品和可能的其他详细信息来创建 OrderLine。在等待时,这些async变体确保一次只有一个线程访问 DbContext,因此存储库可以使用同一个 DbContext 实例。Order、new 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 Factory 类的东西来创建可以模拟出来的 DbContext。关键是 DbContext 的范围必须移动到并行化方法中。AsNoTracking()很重要,因为我们不能让这个 DbContext “跟踪”这个订单;当我们想要保存订单和任何其他关联实体时,我们必须将此订单与新的 DbContext 实例相关联。(这个正在处理中)如果实体仍然认为它被跟踪,那将导致错误。这也意味着存储库 Save 必须更改为更像:

Repository.Save(order);

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

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

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

因此,在上面的示例中,按 ID 加载 Order 并创建 Orderline 将是我通常会保持同步而不是异步的东西。按 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 毫秒或更短的时间内执行的一般查询。(基本上,默认)

异步 - 跨较大集合的较大查询可能会更慢执行时间,例如动态搜索,或者预期会非常频繁地调用的较短操作。

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

于 2021-02-04T22:40:24.760 回答