Entity Framework SaveChanges () so với SaveChangesAsync () và Find () so với FindAsync ()


86

Tôi đã tìm kiếm sự khác biệt giữa 2 cặp trên nhưng không tìm thấy bất kỳ bài báo nào giải thích rõ ràng về nó cũng như khi nào sử dụng cái này hay cái khác.

Vì vậy, sự khác biệt giữa SaveChanges()và là SaveChangesAsync()gì?
Và giữa Find()FindAsync()?

Về phía máy chủ, khi chúng ta sử dụng Asynccác phương thức, chúng ta cũng cần thêm await. Vì vậy, tôi không nghĩ rằng nó là không đồng bộ ở phía máy chủ.

Nó chỉ giúp ngăn chặn việc chặn giao diện người dùng trên trình duyệt phía máy khách? Hay có bất kỳ ưu và nhược điểm nào giữa chúng?


2
async là nhiều, nhiều hơn dừng thread UI khách hàng từ chặn trong các ứng dụng của khách hàng. Tôi chắc rằng sẽ có câu trả lời của chuyên gia trong thời gian ngắn.
jdphenix

Câu trả lời:


174

Bất kỳ lúc nào bạn cần thực hiện một hành động trên máy chủ từ xa, chương trình của bạn sẽ tạo ra yêu cầu, gửi yêu cầu, sau đó chờ phản hồi. Tôi sẽ sử dụng SaveChanges()SaveChangesAsync()làm ví dụ nhưng điều tương tự cũng áp dụng cho Find()FindAsync() .

Giả sử bạn có một danh sách myListhơn 100 mục mà bạn cần thêm vào cơ sở dữ liệu của mình. Để chèn điều đó, hàm của bạn sẽ trông giống như sau:

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

Đầu tiên, bạn tạo một phiên bản MyEDM, thêm danh sách myListvào bảng MyTable, sau đó gọi SaveChanges()để duy trì các thay đổi đối với cơ sở dữ liệu. Nó hoạt động theo cách bạn muốn, các bản ghi được cam kết, nhưng chương trình của bạn không thể làm bất cứ điều gì khác cho đến khi cam kết kết thúc. Quá trình này có thể mất nhiều thời gian tùy thuộc vào những gì bạn đang cam kết. Nếu bạn đang thực hiện các thay đổi đối với các bản ghi, thực thể phải thực hiện những thay đổi đó cùng một lúc (tôi đã từng có một lần lưu mất 2 phút để cập nhật)!

Để giải quyết vấn đề này, bạn có thể làm một trong hai điều. Đầu tiên là bạn có thể khởi động một luồng mới để xử lý phần chèn. Trong khi điều này sẽ giải phóng chuỗi gọi để tiếp tục thực thi, bạn đã tạo một chuỗi mới chỉ cần ngồi đó và chờ đợi. Không cần chi phí cao đó, và đây là những gì async awaitmô hình giải quyết.

Đối với các đối tác I / O, hãy awaitnhanh chóng trở thành người bạn tốt nhất của bạn. Lấy phần mã từ trên xuống, chúng ta có thể sửa đổi nó thành:

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

Đó là một thay đổi rất nhỏ, nhưng có những ảnh hưởng sâu sắc đến hiệu quả và hiệu suất của mã của bạn. Vậy điều gì xảy ra? Đầu mã giống nhau, bạn tạo một phiên bản của MyEDMvà thêm myListvào MyTable. Nhưng khi bạn gọi await context.SaveChangesAsync(), việc thực thi mã trở lại hàm gọi! Vì vậy, trong khi bạn đang đợi tất cả các bản ghi đó cam kết, mã của bạn có thể tiếp tục thực thi. Giả sử hàm chứa đoạn mã trên có chữ ký của public async Task SaveRecords(List<MyTable> saveList), hàm gọi có thể giống như sau:

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

Tại sao bạn lại có một chức năng như thế này, tôi không biết, nhưng những gì nó xuất ra cho thấy cách async awaithoạt động. Đầu tiên chúng ta hãy xem xét những gì sẽ xảy ra.

Việc thực thi đi vào MyCallingFunction, Function Startingsau đó Save Startingđược ghi vào bảng điều khiển, sau đó hàm SaveChangesAsync()được gọi. Tại thời điểm này, việc thực thi quay trở lại MyCallingFunctionvà đi vào vòng lặp for viết 'Tiếp tục thực thi' lên đến 1000 lần. Khi SaveChangesAsync()kết thúc, việc thực thi trở lại SaveRecordshàm, ghi Save Completevào bảng điều khiển. Khi mọi thứ SaveRecordshoàn tất, quá trình thực thi sẽ tiếp tục MyCallingFunctionđúng như khi SaveChangesAsync()hoàn thành. Bối rối? Đây là một ví dụ đầu ra:

Chức năng Bắt đầu
Lưu bắt đầu
Tiếp tục thực hiện!
Tiếp tục thực hiện!
Tiếp tục thực hiện!
Tiếp tục thực hiện!
Tiếp tục thực hiện!
....
Tiếp tục thực hiện!
Lưu Hoàn thành!
Tiếp tục thực hiện!
Tiếp tục thực hiện!
Tiếp tục thực hiện!
....
Tiếp tục thực hiện!
Chức năng hoàn thành!

Hoặc có thể:

Chức năng Bắt đầu
Lưu bắt đầu
Tiếp tục thực hiện!
Tiếp tục thực hiện!
Lưu Hoàn thành!
Tiếp tục thực hiện!
Tiếp tục thực hiện!
Tiếp tục thực hiện!
....
Tiếp tục thực hiện!
Chức năng hoàn thành!

Đó là vẻ đẹp của async await, mã của bạn có thể tiếp tục chạy trong khi bạn đang đợi một thứ gì đó kết thúc. Trong thực tế, bạn sẽ có một chức năng giống như chức năng gọi của bạn:

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

Ở đây, bạn có bốn chức năng lưu bản ghi khác nhau hoạt động cùng một lúc . MyCallingFunctionsẽ hoàn thành nhanh hơn rất nhiều async awaitso với khi các SaveRecordschức năng riêng lẻ được gọi theo chuỗi.

Một điều mà tôi chưa chạm vào là awaittừ khóa. Điều này làm là ngăn chức năng hiện tại thực thi cho đến khi bất cứ điều gì Taskbạn đang chờ hoàn thành. Vì vậy, trong trường hợp của bản gốc MyCallingFunction, dòng Function Completesẽ không được ghi vào bảng điều khiển cho đến khi SaveRecordschức năng kết thúc.

Tóm lại, nếu bạn có một tùy chọn để sử dụng async await, bạn nên sử dụng vì nó sẽ làm tăng đáng kể hiệu suất của ứng dụng của bạn.


7
99% thời gian tôi vẫn phải đợi các giá trị được nhận từ cơ sở dữ liệu trước khi có thể tiếp tục. Tôi vẫn nên sử dụng async? Không đồng bộ có cho phép 100 người kết nối không đồng bộ với trang web của tôi không? Nếu tôi không sử dụng async, điều đó có nghĩa là tất cả 100 người dùng phải đợi ở hàng 1 cùng một lúc?
MIKE

6
Cần lưu ý: Việc sinh ra một luồng mới từ nhóm luồng khiến ASP trở thành một con gấu trúc đáng buồn vì về cơ bản bạn cướp một luồng khỏi ASP (có nghĩa là luồng không thể xử lý các yêu cầu khác hoặc làm bất cứ điều gì vì nó bị mắc kẹt trong lệnh gọi chặn). awaitTuy nhiên, nếu bạn sử dụng , ngay cả khi BẠN không cần làm bất cứ điều gì khác sau lệnh gọi SaveChanges, ASP sẽ nói "aha, chuỗi này được trả về đang chờ thao tác không đồng bộ, điều này có nghĩa là tôi có thể để chuỗi này xử lý một số yêu cầu khác trong thời gian chờ đợi ! " Điều này làm cho ứng dụng của bạn mở rộng theo chiều ngang tốt hơn nhiều.
sara

3
Trên thực tế, tôi đã định chuẩn không đồng bộ để chậm hơn. Và bạn đã bao giờ thấy có bao nhiêu luồng có sẵn trong một máy chủ ASP.Net điển hình chưa? Nó giống như hàng chục ngàn. Vì vậy, khả năng hết luồng để xử lý các yêu cầu khác là rất khó xảy ra và ngay cả khi bạn đã có đủ lưu lượng truy cập để bão hòa tất cả các luồng đó, máy chủ của bạn có thực sự đủ mạnh để không gặp khó khăn trong trường hợp đó không? Khi khẳng định rằng sử dụng async ở mọi nơi làm tăng hiệu suất là hoàn toàn sai. Nó có thể trong một số trường hợp nhất định, nhưng trong hầu hết các tình huống phổ biến, nó sẽ chậm hơn. Điểm chuẩn và xem.
user3766657

@MIKE trong khi một người dùng phải đợi cơ sở dữ liệu trả về dữ liệu để tiếp tục, những người dùng khác sử dụng ứng dụng của bạn thì không. Trong khi IIS tạo một luồng cho mỗi yêu cầu (thực sự phức tạp hơn thế), luồng đang chờ của bạn có thể được sử dụng để xử lý các yêu cầu khác, điều này rất quan trọng đối với khả năng mở rộng. Hình ảnh từng yêu cầu, thay vì sử dụng 1 luồng toàn thời gian, nó sử dụng nhiều luồng ngắn hơn có thể được sử dụng lại ở một nơi khác (hay còn gọi là yêu cầu khác).
Bart Calixto

1
Tôi muốn chỉ thêm rằng bạn nên luôn luôn await cho SaveChangesAsynctừ EF không hỗ trợ nhiều tiết kiệm cùng một lúc. docs.microsoft.com/en-us/ef/core/saving/async Ngoài ra, thực sự có một lợi thế lớn khi sử dụng các phương pháp không đồng bộ này. Ví dụ: bạn có thể tiếp tục nhận các yêu cầu khác trong webApi của mình khi lưu dữ liệu hoặc thực hiện nhiều thao tác hoặc cải thiện trải nghiệm người dùng, không đóng băng giao diện khi bạn đang sử dụng ứng dụng dành cho máy tính để bàn.
tgarcia

1

Phần giải thích còn lại của tôi sẽ dựa trên đoạn mã sau.

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

Trường hợp 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;
}

nhập mô tả hình ảnh ở đây

Nhận xét: Khi phần đồng bộ (màu xanh lá cây) của JobAsyncquay lâu hơn nhiệm vụ t(màu đỏ) thì nhiệm vụ tđã được hoàn thành tại điểm await t. Kết quả là phần tiếp tục (màu xanh lam) chạy trên cùng một luồng với phần màu xanh lá cây. Phần đồng bộ của Main(màu trắng) sẽ quay sau khi phần màu xanh lá cây quay xong. Đó là lý do tại sao phần đồng bộ trong phương thức không đồng bộ là có vấn đề.

Trường hợp 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;
}

nhập mô tả hình ảnh ở đây

Nhận xét: Trường hợp này ngược lại với trường hợp đầu tiên. Phần đồng bộ (màu xanh lá cây) của các JobAsyncvòng quay ngắn hơn nhiệm vụ t(màu đỏ) thì nhiệm vụ tđó chưa được hoàn thành tại điểm await t. Kết quả là, phần tiếp tục (màu xanh lam) chạy trên luồng khác với màu xanh lá cây. Phần đồng bộ của Main(màu trắng) vẫn quay sau khi phần màu xanh lục kết thúc quay.

Trường hợp 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;
}

nhập mô tả hình ảnh ở đây

Nhận xét: Trường hợp này sẽ giải quyết vấn đề trong các trường hợp trước về phần đồng bộ trong phương pháp không đồng bộ. Nhiệm vụ tđược chờ đợi ngay lập tức. Kết quả là, phần tiếp tục (màu xanh lam) chạy trên luồng khác với màu xanh lá cây. Phần đồng bộ của Main(màu trắng) sẽ quay ngay lập tức song song với JobAsync.

Nếu bạn muốn thêm các trường hợp khác, hãy thoải mái chỉnh sửa.


1

Tuyên bố này không chính xác:

Về phía máy chủ, khi chúng tôi sử dụng các phương thức Async, chúng tôi cũng cần thêm await.

Bạn không cần phải thêm "await", awaitchỉ đơn thuần là một từ khóa thuận tiện trong C # cho phép bạn viết nhiều dòng mã hơn sau cuộc gọi và những dòng khác đó sẽ chỉ được thực thi sau khi hoạt động Lưu hoàn tất. Nhưng như bạn đã chỉ ra, bạn có thể thực hiện điều đó đơn giản bằng cách gọi SaveChangesthay vì gọi SaveChangesAsync.

Nhưng về cơ bản, một cuộc gọi không đồng bộ còn nhiều hơn thế nữa. Ý tưởng ở đây là nếu bạn có thể làm công việc khác (trên máy chủ) trong khi thao tác Lưu đang diễn ra, thì bạn nên sử dụng SaveChangesAsync. Không sử dụng "await". Chỉ cần gọi SaveChangesAsync, và sau đó tiếp tục thực hiện các công việc khác song song. Điều này bao gồm khả năng, trong một ứng dụng web, trả lại phản hồi cho khách hàng ngay cả trước khi quá trình Lưu hoàn tất. Nhưng tất nhiên, bạn vẫn sẽ muốn kiểm tra kết quả cuối cùng của Lưu để trong trường hợp không thành công, bạn có thể thông báo điều đó cho người dùng của mình hoặc ghi lại bằng cách nào đó.


4
Bạn thực sự muốn chờ các cuộc gọi này nếu không, bạn có thể chạy các truy vấn và hoặc lưu dữ liệu đồng thời bằng cách sử dụng cùng một cá thể DbContext và DbContext không an toàn cho chuỗi. Trên hết, sự chờ đợi giúp bạn dễ dàng xử lý các trường hợp ngoại lệ. Nếu không chờ đợi, bạn sẽ phải lưu trữ tác vụ và kiểm tra xem nó có bị lỗi không nhưng nếu không biết khi nào tác vụ hoàn thành, bạn sẽ không biết khi nào cần kiểm tra trừ khi bạn sử dụng '.ContinueWith' đòi hỏi nhiều suy nghĩ hơn là chờ đợi.
Pawel

22
Câu trả lời này là lừa dối, Gọi một phương thức không đồng bộ mà không chờ đợi làm cho nó trở thành một "ngọn lửa và quên." Phương thức này hoạt động và có thể sẽ hoàn thành vào một lúc nào đó, nhưng bạn sẽ không bao giờ biết khi nào và nếu nó ném ra một ngoại lệ, bạn sẽ không bao giờ nghe về nó, Bạn không thể đồng bộ hóa với quá trình hoàn thành của nó. Loại hành vi tiềm ẩn nguy hiểm này nên được chọn, không được gọi với một quy tắc đơn giản (và không chính xác) như "đang chờ máy khách, không chờ trên máy chủ."
John Melville

1
Đây là một phần kiến ​​thức rất hữu ích mà tôi đã đọc trong tài liệu, nhưng chưa thực sự xem xét. Vì vậy, bạn có tùy chọn để: 1. SaveChangesAsync () thành "Cháy và quên", như John Melville nói ... điều này rất hữu ích đối với tôi trong một số trường hợp. 2. chờ SaveChangesAsync () để "cháy, trả lại cho người gọi, và sau đó thực hiện một số 'post-tiết kiệm' mã sau khi tiết kiệm được hoàn thành mảnh Rất hữu ích Cảm ơn bạn...
Parrhesia Joe
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.