实体框架SaveChanges()与SaveChangesAsync()和Find()与FindAsync()


86

我一直在寻找以上两对之间的差异,但是没有找到任何清楚地解释它以及何时使用一个或另一个的文章。

那么SaveChanges()和之间有什么区别SaveChangesAsync()
Find()和之间FindAsync()

在服务器端,当我们使用Async方法时,我们还需要添加await。因此,我认为它在服务器端不是异步的。

它仅有助于防止UI在客户端浏览器上阻塞吗?还是它们之间有什么优缺点?


2
异步多,很多不是从客户端应用程序阻止停止客户端UI线程多。我敢肯定,很快就会有专家解答。
jdphenix

Answers:


174

任何时候需要在远程服务器上执行操作时,程序都会生成请求,将其发送,然后等待响应。我将使用SaveChanges()SaveChangesAsync()作为示例,但对Find()和适用相同FindAsync()

假设您有一个myList需要添加到数据库中的100多个项目的列表。要插入它,您的函数应如下所示:

using(var context = new MyEDM())
{
    context.MyTable.AddRange(myList);
    context.SaveChanges();
}

首先,您创建一个实例MyEDM,将列表添加myList到表中MyTable,然后调用SaveChanges()以将更改持久保存到数据库中。它可以按您想要的方式工作,记录被提交,但是您的程序在提交完成之前无法执行其他任何操作。这可能需要很长时间,具体取决于您要提交的内容。如果您要对记录进行更改,则实体必须一次提交一个记录(我曾经保存了2分钟以进行更新)!

要解决此问题,您可以执行以下两项操作之一。首先是您可以启动新线程来处理插入。尽管这将释放调用线程以继续执行,但您创建了一个新线程,该线程将坐在那里等待。不需要这些开销,这就是async await模式所要解决的问题。

对于I / O操作,await迅速成为您最好的朋友。从上面的代码部分,我们可以将其修改为:

using(var context = new MyEDM())
{
    Console.WriteLine("Save Starting");
    context.MyTable.AddRange(myList);
    await context.SaveChangesAsync();
    Console.WriteLine("Save Complete");
}

这是很小的变化,但是对代码的效率和性能产生了深远的影响。那会怎样呢?代码的开头是相同的,您创建的实例MyEDM并将其添加myListMyTable。但是,当您调用时await context.SaveChangesAsync(),代码的执行将返回到调用函数!因此,在等待所有这些记录提交时,您的代码可以继续执行。说包含以上代码的函数具有的签名public async Task SaveRecords(List<MyTable> saveList),调用函数可能如下所示:

public async Task MyCallingFunction()
{
    Console.WriteLine("Function Starting");
    Task saveTask = SaveRecords(GenerateNewRecords());

    for(int i = 0; i < 1000; i++){
        Console.WriteLine("Continuing to execute!");
    }

    await saveTask;
    Console.Log("Function Complete");
}

我不知道为什么会有这样的功能,但是输出的内容说明了它是如何async await工作的。首先,让我们回顾一下会发生什么。

执行进入MyCallingFunctionFunction Starting然后Save Starting写入控制台,然后SaveChangesAsync()调用该函数。此时,执行返回MyCallingFunction并进入for循环,最多写入1000次“ Continue to Execute”。当SaveChangesAsync()完成后,执行返回SaveRecords功能,写Save Complete到控制台。一旦一切都SaveRecords完成了,那么将在完成时继续MyCallingFunction正确执行SaveChangesAsync()。困惑?这是示例输出:

功能启动
保存开始
继续执行!
继续执行!
继续执行!
继续执行!
继续执行!
....
继续执行!
保存完成!
继续执行!
继续执行!
继续执行!
....
继续执行!
功能齐全!

或许:

功能启动
保存开始
继续执行!
继续执行!
保存完成!
继续执行!
继续执行!
继续执行!
....
继续执行!
功能齐全!

这就是它的美async await,您可以在等待完成的过程中继续运行代码。实际上,您将有一个类似于以下的函数作为调用函数:

public async Task MyCallingFunction()
{
    List<Task> myTasks = new List<Task>();
    myTasks.Add(SaveRecords(GenerateNewRecords()));
    myTasks.Add(SaveRecords2(GenerateNewRecords2()));
    myTasks.Add(SaveRecords3(GenerateNewRecords3()));
    myTasks.Add(SaveRecords4(GenerateNewRecords4()));

    await Task.WhenAll(myTasks.ToArray());
}

在这里,你有四个不同的保存记录功能会在同一时间。与单个函数被串行调用相比,MyCallingFunction使用它可以更快地完成。async awaitSaveRecords

我尚未触及的一件事是await关键字。这样做是阻止当前函数执行,直到Task您等待的内容完成。因此,对于原始版本而言MyCallingFunctionFunction CompleteSaveRecords功能完成之前,该行不会被写入控制台。

长话短说,如果您可以选择使用async await,则应该这样做,因为它将大大提高应用程序的性能。


7
99%的时间我仍然必须等待从数据库接收值,然后才能继续。我应该仍然使用异步吗?异步是否允许100个人异步连接到我的网站?如果我不使用异步,那是否意味着所有100个用户必须一次在第1行中等待?
MIKE

6
值得注意的是:从线程池中产生一个新线程会使ASP变成悲伤的熊猫,因为您基本上是从ASP抢劫了一个线程(这意味着该线程无法处理其他请求,也无法执行任何操作,因为它被阻塞在阻塞调用中)。await但是,如果您使用它,即使在调用SaveChanges之后不需要执行任何其他操作,ASP也会说“啊哈,此线程返回以等待异步操作,这意味着我可以让此线程同时处理其他一些请求!” 这使您的应用水平扩展更好。
萨拉

3
实际上,我已经将异步基准测试的速度降低了。您是否曾见过典型的ASP.Net服务器中有多少个线程可用?就像成千上万。因此,用尽线程来处理更多请求的可能性很小,即使您确实有足够的流量来饱和所有这些线程,您的服务器是否真的足够强大以至于在这种情况下也不会崩溃?声称在各处使用异步会提高性能是完全错误的。在某些情况下可以这样做,但实际上在大多数情况下它会更慢。进行基准测试。
user3766657 '16

@MIKE虽然单个用户必须等待数据库返回数据才能继续,但是使用您的应用程序的其他用户却没有。虽然IIS为每个请求创建一个线程(实际上比它复杂),但您的等待线程可用于处理其他请求,这对于可扩展性afaik很重要。对每个请求进行映像,而不是全时使用1个线程,而是使用许多较短的线程,这些线程可以在其他地方(也称为其他请求)重用。
Bart Calixto

1
我只想补充一点,因为EF不同时支持多个保存,因此您应该始终 这样做。 docs.microsoft.com/en-us/ef/core/saving/async 此外,使用这些异步方法实际上也有很大的优势。例如,当保存数据或进行大量修改时,您可以在webApi中继续接收其他请求,或者在桌面应用程序中改善用户体验而不冻结界面。awaitSaveChangesAsync
tgarcia

1

我剩下的解释将基于以下代码片段。

using System;
using System.Threading;
using System.Threading.Tasks;
using static System.Console;

public static class Program
{
    const int N = 20;
    static readonly object obj = new object();
    static int counter;

    public static void Job(ConsoleColor color, int multiplier = 1)
    {
        for (long i = 0; i < N * multiplier; i++)
        {
            lock (obj)
            {
                counter++;
                ForegroundColor = color;
                Write($"{Thread.CurrentThread.ManagedThreadId}");
                if (counter % N == 0) WriteLine();
                ResetColor();
            }
            Thread.Sleep(N);
        }
    }

    static async Task JobAsync()
    {
       // intentionally removed
    }

    public static async Task Main()
    {
       // intentionally removed
    }
}

情况1

static async Task JobAsync()
{
    Task t = Task.Run(() => Job(ConsoleColor.Red, 1));
    Job(ConsoleColor.Green, 2);
    await t;
    Job(ConsoleColor.Blue, 1);
}

public static async Task Main()
{
    Task t = JobAsync();
    Job(ConsoleColor.White, 1);
    await t;
}

在此处输入图片说明

备注:由于JobAsync旋转的同步部分(绿色)比任务t(红色)旋转的时间长,因此任务t已在的点完成await t。结果,延续(蓝色)在与绿色线程相同的线程上运行。Main绿色的纺丝完成后,(白色)的同步部分将旋转。这就是为什么异步方法中的同步部分有问题的原因。

情况二

static async Task JobAsync()
{
    Task t = Task.Run(() => Job(ConsoleColor.Red, 2));
    Job(ConsoleColor.Green, 1);
    await t;
    Job(ConsoleColor.Blue, 1);
}

public static async Task Main()
{
    Task t = JobAsync();
    Job(ConsoleColor.White, 1);
    await t;
}

在此处输入图片说明

备注:这种情况与第一种情况相反。JobAsync自旋的同步部分(绿色)比任务t(红色)短,则任务t尚未在的位置完成await t。结果,延续(蓝色)在与绿色线程不同的线程上运行。Main绿色的纺丝完成后,(白色)的同步部分仍然旋转。

情况3

static async Task JobAsync()
{
    Task t = Task.Run(() => Job(ConsoleColor.Red, 1));
    await t;
    Job(ConsoleColor.Green, 1);
    Job(ConsoleColor.Blue, 1);
}

public static async Task Main()
{
    Task t = JobAsync();
    Job(ConsoleColor.White, 1);
    await t;
}

在此处输入图片说明

备注:这种情况将解决以前情况下有关异步方法中的同步部分的问题。t立即等待任务。结果,延续(蓝色)在与绿色线程不同的线程上运行。Main(白色)的同步部分将立即平行于旋转JobAsync

如果要添加其他案例,请随时进行编辑。


1

此语句不正确:

在服务器端,当我们使用异步方法时,我们还需要添加await。

您不需要添加“ await”,await它只是C#中的一个便捷关键字,使您可以在调用之后编写更多行代码,而其他行仅在Save操作完成后才执行。但是正如您所指出的,您可以简单地通过调用SaveChanges而不是来实现SaveChangesAsync

但从根本上讲,异步调用远不止于此。这里的想法是,如果在“保存”操作进行期间还有其他工作(在服务器上)可以执行,则应使用SaveChangesAsync。不要使用“等待”。只需调用SaveChangesAsync,然后继续并行执行其他操作即可。这可能包括在Web应用程序中甚至在保存完成之前就将响应返回给客户端。但是,当然,您仍然希望检查保存的最终结果,以便万一失败,您可以将其传达给用户或以某种方式记录下来。


4
您实际上是想等待这些调用,否则您可能会使用相同的DbContext实例运行查询和/或同时保存数据,并且DbContext不是线程安全的。最重要的是,等待使处理异常变得容易。如果没有等待,您将不得不存储任务并检查它是否出错,但是如果您不知道任务何时完成,就不会知道何时进行检查,除非您使用“ .ContinueWith”,这比等待需要更多的思考。
Pawel 2015年

22
这个答案是欺骗性的,在不等待的情况下调用异步方法将其称为“即发即弃”。该方法将关闭并且可能会在某个时间完成,但是您将永远不知道何时,并且如果它引发异常,您将永远不会听说它,您将无法与它的完成同步。应该选择这种潜在的危险行为,而不要使用简单的(不正确的)规则来调用,例如“在客户端上唤醒,在服务器上不等待”。
约翰·梅尔维尔

1
这是我在文档中阅读的非常有用的知识,但并未真正考虑。因此,您可以选择:1.如John Melville所说,将SaveChangesAsync()设置为“解雇”,这在某些情况下对我很有用。2.的await SaveChangesAsync(),以“火,返回给调用者,然后执行一些‘后保存’代码保存完成后,非常有用的一块谢谢。
Parrhesia乔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.