【问题标题】:Entity Framework SaveChanges() vs. SaveChangesAsync() and Find() vs. FindAsync()实体框架 SaveChanges() 与 SaveChangesAsync() 和 Find() 与 FindAsync()
【发布时间】:2015-05-05 01:26:07
【问题描述】:

我一直在寻找上述两对之间的区别,但没有找到任何文章清楚地解释它以及何时使用一对。

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

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

它是否仅有助于防止客户端浏览器上的 UI 阻塞?或者它们之间有什么优缺点?

【问题讨论】:

  • 异步不仅仅是阻止客户端 UI 线程阻塞客户端应用程序。我相信很快就会有专家解答。

标签: c# entity-framework async-await


【解决方案1】:

只要您需要在远程服务器上执行操作,您的程序就会生成请求、发送请求,然后等待响应。我将以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 的实例并将您的myList 添加到MyTable。但是当您调用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循环写入'Continuing to Execute'最多1000次。当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 await 将比连续调用单个 SaveRecords 函数快得多。

我还没有提到的一件事是await 关键字。它的作用是停止当前函数的执行,直到您等待的 Task 完成为止。所以对于原始的MyCallingFunction,在SaveRecords 函数完成之前,Function Complete 行不会被写入控制台。

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

【讨论】:

  • 99% 的时间我仍然需要等待从数据库接收到值才能继续。我还应该使用异步吗? async 是否允许 100 人异步连接到我的网站?如果我不使用异步,这是否意味着所有 100 个用户都必须一次在第 1 行等待?
  • 值得注意的是:从线程池中生成一个新线程使 ASP 成为一个悲伤的熊猫,因为您基本上从 ASP 中抢走了一个线程(这意味着该线程无法处理其他请求或做任何事情,因为它是卡在阻塞呼叫中)。但是,如果您使用await,即使您在调用 SaveChanges 后不需要做任何其他事情,ASP 也会说“啊哈,这个线程返回等待异步操作,这意味着我可以让这个线程处理一些其他请求同时!”这使您的应用可以更好地横向扩展。
  • 实际上我已经对异步进行了基准测试,使其变慢。您是否见过典型的 ASP.Net 服务器中有多少线程可用?这就像数以万计。因此,用完线程来处理进一步的请求的可能性很小,即使您确实有足够的流量使所有这些线程饱和,您的服务器是否真的足够强大以至于不会在这种情况下崩溃?声称在任何地方使用异步可以提高性能是完全错误的。在某些情况下它可以,但在大多数常见情况下它实际上会更慢。基准测试并查看。
  • @MIKE 虽然单个用户必须等待数据库返回数据才能继续,但使用您的应用程序的其他用户则不需要。虽然 IIS 为每个请求创建一个线程(实际上它比这更复杂),但您的等待线程可用于处理其他请求,这对于可伸缩性 afaik 很重要。对每个请求进行成像,而不是全时使用 1 个线程,而是使用许多较短的线程,这些线程可以在其他地方重用(也称为另一个请求)。
  • 我想补充一点,您应该始终 await for SaveChangesAsync,因为 EF 不支持同时进行多个保存。 docs.microsoft.com/en-us/ef/core/saving/async 此外,使用这些异步方法实际上有很大的优势。例如,您可以在保存数据或执行大量操作时在 webApi 中继续接收其他请求,或者在桌面应用程序中改善用户体验而不冻结界面。
【解决方案2】:

我剩下的解释将基于以下代码sn-p。

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
    }
}

案例一

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(白色)的同步部分将在绿色旋转完成后旋转。这就是为什么异步方法中的同步部分有问题的原因。

案例 2

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(红色)旋转的时间短,然后任务 tawait 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 平行旋转。

如果您想添加其他案例,请随时编辑。

【讨论】:

    【解决方案3】:

    此说法不正确:

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

    您不需要添加“等待”,await 只是 C# 中的一个方便关键字,它使您可以在调用后编写更多行代码,而其他行只有在保存操作完成后才会执行。但正如您所指出的,您只需调用 SaveChanges 而不是 SaveChangesAsync 即可完成此操作。

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

    【讨论】:

    • 您实际上希望等待这些调用,否则您可能会使用相同的 DbContext 实例同时运行查询和/或保存数据,并且 DbContext 不是线程安全的。最重要的是,等待可以轻松处理异常。如果没有 await,您将不得不存储任务并检查它是否有问题,但如果不知道任务何时完成,您将不知道何时检查,除非您使用 '.ContinueWith',这比 await 需要更多的思考。
    • 这个答案具有欺骗性,在没有等待的情况下调用异步方法使其成为“一劳永逸”。该方法关闭并且可能会在某个时候完成,但您永远不知道何时,如果它抛出异常,您将永远不会听到它,您无法与其完成同步。应该选择这种具有潜在危险的行为,而不是使用简单(且不正确)的规则来调用,例如“在客户端等待,在服务器上不等待”。
    • 这是我在文档中读过但没有真正考虑过的非常有用的知识。因此,您可以选择: 1. SaveChangesAsync() 到“一劳永逸”,就像 John Melville 所说的那样……这在某些情况下对我很有用。 2. await SaveChangesAsync() to "触发,返回调用者,然后在保存完成后执行一些'post-save'代码。非常有用的一段。谢谢。
    • 这个帖子真的是错误的,你应该总是使用SaveChangesAsync,即使你不需要解雇和忘记。调用SaveChanges 会启动新线程并浪费服务器资源。
    猜你喜欢
    • 2023-04-07
    • 1970-01-01
    • 2015-02-28
    • 1970-01-01
    • 2016-09-05
    • 1970-01-01
    • 1970-01-01
    • 2023-01-29
    相关资源
    最近更新 更多