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

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

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

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

它是否只有助于防止客户端浏览器的 UI 阻塞?还是他们之间有利有弊?

108740 次浏览

This statement is incorrect:

On server side, when we use Async methods, we also need to add await.

You do not need to add "await", await is merely a convenient keyword in C# that enables you to write more lines of code after the call, and those other lines will only get executed after the Save operation completes. But as you pointed out, you could accomplish that simply by calling SaveChanges instead of SaveChangesAsync.

But fundamentally, an async call is about much more than that. The idea here is that if there is other work you can do (on the server) while the Save operation is in progress, then you should use SaveChangesAsync. Do not use "await". Just call SaveChangesAsync, and then continue to do other stuff in parallel. This includes potentially, in a web app, returning a response to the client even before the Save has completed. But of course, you still will want to check the final result of the Save so that in case it fails, you can communicate that to your user, or log it somehow.

Any time that you need to do an action on a remote server, your program generates the request, sends it, then waits for a response. I will use SaveChanges() and SaveChangesAsync() as an example but the same applies to Find() and FindAsync().

Say you have a list myList of 100+ items that you need to add to your database. To insert that, your function would look something like so:

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

First you create an instance of MyEDM, add the list myList to the table MyTable, then call SaveChanges() to persist the changes to the database. It works how you want, the records get committed, but your program cannot do anything else until the commit finishes. This can take a long time depending on what you are committing. If you are committing changes to the records, entity has to commit those one at a time (I once had a save take 2 minutes for updates)!

To solve this problem, you could do one of two things. The first is you can start up a new thread to handle the insert. While this will free up the calling thread to continue executing, you created a new thread that is just going to sit there and wait. There is no need for that overhead, and this is what the async await pattern solves.

For I/O opperations, await quickly becomes your best friend. Taking the code section from above, we can modify it to be:

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

It is a very small change, but there are profound effects on the efficiency and performance of your code. So what happens? The begining of the code is the same, you create an instance of MyEDM and add your myList to MyTable. But when you call await context.SaveChangesAsync(), the execution of code returns to the calling function! So while you are waiting for all those records to commit, your code can continue to execute. Say the function that contained the above code had the signature of public async Task SaveRecords(List<MyTable> saveList), the calling function could look like this:

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");
}

Why you would have a function like this, I don't know, but what it outputs shows how async await works. First let's go over what happens.

Execution enters MyCallingFunction, Function Starting then Save Starting gets written to the console, then the function SaveChangesAsync() gets called. At this point, execution returns to MyCallingFunction and enters the for loop writing 'Continuing to Execute' up to 1000 times. When SaveChangesAsync() finishes, execution returns to the SaveRecordsfunction, writing Save Complete to the console. Once everything in SaveRecords completes, execution will continue in MyCallingFunction right were it was when SaveChangesAsync() finished. Confused? Here is an example output:

Function Starting
Save Starting
Continuing to execute!
Continuing to execute!
Continuing to execute!
Continuing to execute!
Continuing to execute!
....
Continuing to execute!
Save Complete!
Continuing to execute!
Continuing to execute!
Continuing to execute!
....
Continuing to execute!
Function Complete!

Or maybe:

Function Starting
Save Starting
Continuing to execute!
Continuing to execute!
Save Complete!
Continuing to execute!
Continuing to execute!
Continuing to execute!
....
Continuing to execute!
Function Complete!

That is the beauty of async await, your code can continue to run while you are waiting for something to finish. In reality, you would have a function more like this as your calling function:

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());
}

Here, you have four different save record functions going at the same time. MyCallingFunction will complete a lot faster using async await than if the individual SaveRecords functions were called in series.

The one thing that I have not touched on yet is the await keyword. What this does is stop the current function from executing until whatever Task you are awaiting completes. So in the case of the original MyCallingFunction, the line Function Complete will not be written to the console until the SaveRecords function finishes.

Long story short, if you have an option to use async await, you should as it will greatly increase the performance of your application.

My remaining explanation will be based on the following code snippet.

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

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

enter image description here

Remarks: As the synchronous part (green) of JobAsync spins longer than the task t (red) then the task t is already completed at the point of await t. As a result, the continuation (blue) runs on the same thread as the green one. The synchronous part of Main (white) will spin after the green one is finished spinning. That is why the synchronous part in asynchronous method is problematic.

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

enter image description here

Remarks: This case is opposite to the first case. The synchronous part (green) of JobAsync spins shorter than the task t (red) then the task t has not been completed at the point of await t. As a result, the continuation (blue) runs on the different thread as the green one. The synchronous part of Main (white) still spins after the green one is finished spinning.

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

enter image description here

Remarks: This case will solve the problem in the previous cases about the synchronous part in asynchronous method. The task t is immediately awaited. As a result, the continuation (blue) runs on the different thread as the green one. The synchronous part of Main (white) will spin immediately parallel to JobAsync.

If you want to add other cases, feel free to edit.