DBContext 中的 await 和 LINQ

await and LINQ within DBContext

我们的应用程序中有一个服务层,它由三个逻辑层组成 - Web 服务、业务模型服务(我们对执行业务逻辑和协调对各种存储库的调用的层的名称)和存储库层使用 EF6 连接到各种数据库。

我们的许多存储库调用只是通过 ToListAsync、FirstOrDefaultAsync 直接从数据库集中获取数据,如下所示:

public async Task<MyObject> GetSomeData()
{
    using(var context = new myDBContext())
    {
        return await context.SomeDbSet.FirstOrDefault(o=>o.Something == true);  
    }
}

关于在这里使用 await 是否正确,我们有一些内部争论,因为在 await 之后此方法中没有任何执行。 I/we 了解代码的编写方式,这是必要的,否则一旦方法存在,上下文就会被处理掉,这将导致异常。但是如果我们在这里等待,我们必须一直等待我们的调用堆栈向上(或向下,取决于您如何看待它),这将导致许多昂贵且有些不必要的上下文切换。

此处的另一个选项是使存储库方法同步,并在调用存储库方法的方法中执行 Task.Run(),例如:

Task.Run(() => MyRepository.GetSomeData());

如果需要,我们可以等待此调用,或者只是 return 将任务对象再次发送给调用者。这里的缺点是对数据库的调用然后变成同步的,并且池中的一个线程在整个数据库调用过程中都处于阻塞状态。

所以这归结为什么更贵?通过 await 或让线程阻塞进行不必要的上下文切换?似乎没有正确答案,但有没有最佳实践?

如有任何想法,我们将不胜感激。

.NET Framework 中有多种“上下文”:LogicalCallContext、SynchronizationContext、HostExecutionContext、SecurityContext、ExecutionContext 等。使用 Async/Await 时会捕获 SynchronizationContext,但它并不是唯一的上下文被抓获。与 SynchronizationContext 一起,ExecutionContext 也被捕获。 ExecutionContext 由 SecurityContext、LogicalCallContext 等组成。

始终针对捕获的 ExecutionContext 执行异步代码。当 await 完成时,如果有一个当前的 SynchronizationContext 被捕获,表示异步方法的其余部分的延续将 posted 到那个 SynchronizationContext。

所以在Task.Run下执行代码时,只有SynchronizationContext不会被捕获,但是ExecutionContext无论如何还是会被捕获。您可以获得相同的行为,即在等待时使用 ConfigureAwait(false) 不让 async/await 捕获 SynchronizationContext。不利的一面是,当 await 完成时,SynchronizationContext 将被忽略,框架将尝试在之前的异步操作完成的地方继续执行,这正是您想要的。

所以在你的场景中,我认为你应该使用 async/await 和 ConfigureAwait(false),因为在这种情况下不会有任何 SynchronizationContext 的开销,同时不会有阻塞的开销任何线程。

以下 post 可能有助于获得更多见解:https://msdn.microsoft.com/en-us/magazine/hh456402.aspx

当然,您应该使用异步版本。

正如您所说,如果您不这样做,await 您将在操作完成之前处理上下文,但这并不意味着调用方法也需要使用 async-await。他们可以 return 完成您在 Task.Run 选项中提到的任务:

public Task<MyObject> FooAsync()
{
    // do some stuff

    return GetSomeDataAsync();
}

public async Task<MyObject> GetSomeDataAsync()
{
    using(var context = new myDBContext())
    {
        return await context.SomeDbSet.FirstOrDefault(o=>o.Something == true);  
    }
}

您提到这种情况下的成本是一些昂贵的上下文切换。我不确定你的意思,但如果你指的是线程上下文切换,那么只有一个。调用线程将在等待异步操作时被释放,另一个线程将在该操作完成时继续 运行。

不仅与执行实际操作所花费的时间相比,这可以忽略不计,如果您使用 Task.Run,您将拥有与从 [=25= 中取出阻塞线程相同的上下文切换].

在同步操作上使用 Task.Run 是多余的。它只是阻塞一个线程,它可能需要比异步等价物更多的上下文切换。