เอนทิตีเฟรมเวิร์ก SaveChanges () เทียบกับ SaveChangesAsync () และ Find () เทียบกับ FindAsync ()


89

ฉันได้ค้นหาความแตกต่างระหว่าง 2 คู่ข้างต้น แต่ไม่พบบทความใด ๆ ที่อธิบายอย่างชัดเจนเกี่ยวกับเรื่องนี้รวมถึงเวลาที่ควรใช้อย่างใดอย่างหนึ่ง

แล้วอะไรคือความแตกต่างระหว่างSaveChanges()และSaveChangesAsync()?
และระหว่างFind()และFindAsync()?

ในฝั่งเซิร์ฟเวอร์เมื่อเราใช้Asyncวิธีการเราต้องเพิ่มawaitไฟล์. ดังนั้นฉันไม่คิดว่ามันเป็นแบบอะซิงโครนัสที่ฝั่งเซิร์ฟเวอร์

ช่วยป้องกัน UI ที่ปิดกั้นบนเบราว์เซอร์ฝั่งไคลเอ็นต์เท่านั้นหรือไม่? หรือมีข้อดีข้อเสียระหว่างกันหรือไม่?


2
async มาก, มากขึ้นกว่าการหยุดด้ายลูกค้า UI จากการปิดกั้นในการใช้งานของลูกค้า ฉันแน่ใจว่าจะมีคำตอบจากผู้เชี่ยวชาญในไม่ช้า
jdphenix

คำตอบ:


176

เมื่อใดก็ตามที่คุณต้องดำเนินการกับเซิร์ฟเวอร์ระยะไกลโปรแกรมของคุณจะสร้างคำขอส่งไปแล้วรอการตอบกลับ ฉันจะใช้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ทำงานอย่างไร ก่อนอื่นมาดูกันว่าเกิดอะไรขึ้น

การดำเนินการเข้าสู่MyCallingFunction, Function Startingแล้วSave Startingก็จะเขียนลงคอนโซลแล้วฟังก์ชั่นSaveChangesAsync()ได้รับการเรียก ณ จุดนี้การดำเนินการจะกลับไปที่MyCallingFunctionและเข้าสู่การเขียนสำหรับลูป 'ดำเนินการต่อเพื่อดำเนินการ' สูงสุด 1,000 ครั้ง เมื่อ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บรรทัดFunction Completeจะไม่ถูกเขียนลงในคอนโซลจนกว่าSaveRecordsฟังก์ชันจะเสร็จสิ้น

เรื่องสั้นขนาดยาวหากคุณมีตัวเลือกในการใช้งานasync awaitคุณควรจะเพิ่มประสิทธิภาพของแอปพลิเคชันของคุณอย่างมาก


7
99% ของเวลาที่ฉันยังคงต้องรอให้ได้รับค่าจากฐานข้อมูลก่อนจึงจะดำเนินการต่อได้ ฉันควรใช้ async หรือไม่ async อนุญาตให้ 100 คนเชื่อมต่อกับเว็บไซต์ของฉันแบบอะซิงโครนัสหรือไม่ ถ้าฉันไม่ใช้ async หมายความว่าผู้ใช้ทั้ง 100 คนต้องรอในบรรทัดที่ 1 ต่อครั้งหรือไม่?
MIKE

6
สิ่งที่น่าสังเกต: การวางไข่เธรดใหม่จากเธรดพูลทำให้ ASP เป็นแพนด้าที่น่าเศร้าเนื่องจากคุณปล้นเธรดจาก ASP โดยทั่วไป (หมายความว่าเธรดไม่สามารถจัดการคำขออื่น ๆ หรือทำอะไรได้เลยเนื่องจากติดอยู่ในการโทรบล็อก) awaitอย่างไรก็ตามหากคุณใช้แม้ว่าคุณจะไม่จำเป็นต้องทำอะไรอีกหลังจากเรียก SaveChanges แล้ว ASP จะพูดว่า "aha เธรดนี้ส่งคืนรอการดำเนินการ async ซึ่งหมายความว่าฉันสามารถปล่อยให้เธรดนี้จัดการคำขออื่น ๆ ได้ในระหว่างนี้ !” สิ่งนี้ทำให้แอปของคุณปรับขนาดในแนวนอนได้ดีขึ้นมาก
sara

3
อันที่จริงฉันเปรียบเทียบ async ให้ช้าลง และคุณเคยเห็นไหมว่าเซิร์ฟเวอร์ ASP.Net ทั่วไปมีกี่เธรด? ก็เหมือนหลายหมื่น ดังนั้นโอกาสในการหมดเธรดเพื่อจัดการกับคำขอเพิ่มเติมจึงไม่น่าเป็นไปได้มากและแม้ว่าคุณจะมีปริมาณการใช้งานเพียงพอที่จะทำให้เธรดทั้งหมดอิ่มตัวเซิร์ฟเวอร์ของคุณมีประสิทธิภาพมากพอที่จะไม่หักล้างในกรณีนั้นหรือไม่? การอ้างว่าการใช้ async ทุกที่ที่เพิ่มประสิทธิภาพนั้นผิดทั้งหมด สามารถทำได้ในบางสถานการณ์ แต่ในสถานการณ์ทั่วไปส่วนใหญ่จะช้ากว่า เกณฑ์มาตรฐานและดู
user3766657

@MIKE ในขณะที่ผู้ใช้รายเดียวต้องรอให้ฐานข้อมูลส่งคืนข้อมูลเพื่อดำเนินการต่อผู้ใช้รายอื่นที่ใช้แอปพลิเคชันของคุณไม่ทำเช่นนั้น ในขณะที่ IIS สร้างเธรดสำหรับแต่ละคำขอ (จริงๆแล้วมันซับซ้อนกว่านั้น) เธรดที่รอคอยของคุณสามารถใช้เพื่อจัดการกับคำขออื่น ๆ ได้ซึ่งเป็นสิ่งสำคัญสำหรับความสามารถในการปรับขนาดได้ การสร้างภาพแต่ละคำขอแทนที่จะใช้ 1 เธรดเต็มเวลาจะใช้เธรดที่สั้นกว่าจำนวนมากซึ่งสามารถนำกลับมาใช้ที่อื่นได้ (เรียกอีกอย่างว่าการร้องขออื่น)
Bart Calixto

1
ฉันต้องการเพียงเพื่อเพิ่มที่คุณควร awaitสำหรับSaveChangesAsyncตั้งแต่ EF ไม่สนับสนุนหลายประหยัดในเวลาเดียวกัน docs.microsoft.com/en-us/ef/core/saving/async นอกจากนี้ยังมีข้อดีอย่างมากในการใช้วิธีการ async เหล่านี้ ตัวอย่างเช่นคุณสามารถรับคำขออื่น ๆ ใน webApi ของคุณได้ตลอดเวลาเมื่อบันทึกข้อมูลหรือดำเนินการต่อเนื่องจำนวนมากหรือปรับปรุงประสบการณ์ของผู้ใช้โดยไม่ให้อินเทอร์เฟซค้างเมื่อคุณอยู่ในแอปพลิเคชันเดสก์ท็อป
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(สีขาว) จะหมุนหลังจากที่สีเขียวเสร็จสิ้นการหมุน นั่นคือสาเหตุที่ส่วนซิงโครนัสในวิธีอะซิงโครนัสมีปัญหา

กรณีที่ 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(สีแดง) แสดงว่างาน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ทันที

หากคุณต้องการเพิ่มกรณีอื่น ๆ อย่าลังเลที่จะแก้ไข


0

คำสั่งนี้ไม่ถูกต้อง:

ในฝั่งเซิร์ฟเวอร์เมื่อเราใช้วิธี Async เราต้องเพิ่มการรอ

คุณไม่จำเป็นต้องเพิ่มคำว่า "await" awaitเป็นเพียงคีย์เวิร์ดที่สะดวกใน C # ซึ่งช่วยให้คุณสามารถเขียนโค้ดได้มากขึ้นหลังจากการโทรและบรรทัดอื่น ๆ เหล่านั้นจะได้รับการดำเนินการหลังจากการดำเนินการบันทึกเสร็จสิ้นเท่านั้น แต่เป็นคุณชี้ให้เห็นคุณจะประสบความสำเร็จที่เพียงโดยการเรียกแทนSaveChangesSaveChangesAsync

แต่โดยพื้นฐานแล้วการโทรแบบ async นั้นมีมากกว่านั้น ความคิดที่นี่คือว่าถ้ามีงานอื่น ๆ ที่คุณสามารถทำได้ (บนเซิร์ฟเวอร์) SaveChangesAsyncในขณะที่การดำเนินการบันทึกอยู่ในความคืบหน้าแล้วคุณควรใช้ อย่าใช้ "รอ" แค่โทรSaveChangesAsyncแล้วทำอย่างอื่นควบคู่ไปด้วย ซึ่งรวมถึงในเว็บแอปที่ส่งคืนการตอบกลับไปยังไคลเอ็นต์ก่อนที่การบันทึกจะเสร็จสิ้น แต่แน่นอนว่าคุณยังคงต้องการตรวจสอบผลลัพธ์สุดท้ายของการบันทึกเพื่อที่ว่าในกรณีที่ล้มเหลวคุณสามารถสื่อสารกับผู้ใช้ของคุณหรือเข้าสู่ระบบได้


5
คุณต้องการรอการโทรเหล่านี้ไม่เช่นนั้นคุณอาจรันคิวรีและหรือบันทึกข้อมูลพร้อมกันโดยใช้อินสแตนซ์ DbContext เดียวกันและ DbContext ไม่ปลอดภัยเธรด ยิ่งไปกว่านั้นการรอคอยทำให้ง่ายต่อการจัดการกับข้อยกเว้น โดยไม่ต้องรอคุณจะต้องจัดเก็บงานและตรวจสอบว่ามีข้อผิดพลาดหรือไม่ แต่ไม่รู้ว่างานเสร็จเมื่อใดคุณจะไม่รู้ว่าจะตรวจสอบเมื่อใดเว้นแต่คุณจะใช้ '.ContinueWith' ซึ่งต้องใช้ความคิดมากกว่ารอ
Pawel

24
คำตอบนี้เป็นการหลอกลวงการเรียกใช้วิธี async โดยไม่ต้องรอจะทำให้เป็น "ไฟและลืม" วิธีนี้ดับลงและอาจจะเสร็จสมบูรณ์ในบางครั้ง แต่คุณจะไม่มีทางรู้ว่าเมื่อใดและหากมีข้อยกเว้นที่คุณจะไม่ได้ยินเกี่ยวกับเรื่องนี้คุณจะไม่สามารถซิงโครไนซ์กับความสมบูรณ์ของมันได้ ควรเลือกพฤติกรรมที่อาจเป็นอันตรายแบบนี้ไม่ใช้กฎง่ายๆ (และไม่ถูกต้อง) เช่น "รอไคลเอ็นต์ไม่ต้องรอบนเซิร์ฟเวอร์"
John Melville

1
นี่เป็นความรู้ที่มีประโยชน์มากที่ฉันได้อ่านในเอกสาร แต่ไม่ได้พิจารณาจริงๆ ดังนั้นคุณมีตัวเลือกในการ 1. SaveChangesAsync () เป็น "Fire and forget" เหมือนที่ John Melville พูด ... ซึ่งมีประโยชน์กับฉันในบางกรณี 2. รอ SaveChangesAsync () ไปที่ "Fire, กลับไปที่ผู้โทรจากนั้นเรียกใช้โค้ด" post-save "หลังจากบันทึกเสร็จเรียบร้อยแล้วชิ้นส่วนที่มีประโยชน์มากขอบคุณ
Parrhesia Joe
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.