เอนทิตีเฟรมเวิร์ก Queryable async


99

ฉันกำลังทำงานกับบางสิ่งบางอย่างของ Web API โดยใช้ Entity Framework 6 และหนึ่งในวิธีการควบคุมของฉันคือ "รับทั้งหมด" ที่คาดว่าจะได้รับเนื้อหาของตารางจากฐานข้อมูลของฉันเป็นIQueryable<Entity>. ในที่เก็บของฉันฉันสงสัยว่ามีเหตุผลที่เป็นประโยชน์หรือไม่ที่จะทำสิ่งนี้แบบอะซิงโครนัสเนื่องจากฉันเพิ่งเริ่มใช้ EF กับ async

โดยทั่วไปจะเดือดถึง

 public async Task<IQueryable<URL>> GetAllUrlsAsync()
 {
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
 }

เทียบกับ

 public IQueryable<URL> GetAllUrls()
 {
    return context.Urls.AsQueryable();
 }

เวอร์ชัน async จะให้ผลประโยชน์ด้านประสิทธิภาพที่นี่จริงหรือฉันมีค่าใช้จ่ายที่ไม่จำเป็นโดยการฉายไปที่รายการก่อน (โดยใช้ async ในใจคุณ) และจากนั้นจะเป็น IQueryable?


1
บริบท URL เป็นประเภท DbSet <URL> ซึ่งใช้ IQueryable <URL> ดังนั้น .AsQueryable () จึงซ้ำซ้อน msdn.microsoft.com/en-us/library/gg696460(v=vs.113).aspx สมมติว่าคุณทำตามรูปแบบที่ EF จัดเตรียมไว้หรือใช้เครื่องมือที่สร้างบริบทให้คุณ
Sean B

คำตอบ:


229

ปัญหาดูเหมือนว่าคุณเข้าใจผิดว่า async / await ทำงานกับ Entity Framework อย่างไร

เกี่ยวกับ Entity Framework

ลองดูรหัสนี้:

public IQueryable<URL> GetAllUrls()
{
    return context.Urls.AsQueryable();
}

และตัวอย่างการใช้งาน:

repo.GetAllUrls().Where(u => <condition>).Take(10).ToList()

เกิดอะไรขึ้นที่นั่น?

  1. เราได้รับIQueryableวัตถุ (ยังไม่ได้เข้าถึงฐานข้อมูล) โดยใช้repo.GetAllUrls()
  2. เราสร้างIQueryableวัตถุใหม่ด้วยเงื่อนไขที่ระบุโดยใช้.Where(u => <condition>
  3. เราสร้างIQueryableวัตถุใหม่ที่มีขีด จำกัด การเพจที่ระบุโดยใช้.Take(10)
  4. .ToList()เราเรียกผลจากฐานข้อมูลโดยใช้ IQueryableวัตถุของเราถูกคอมไพล์เป็น sql (like select top 10 * from Urls where <condition>) และฐานข้อมูลสามารถใช้ดัชนีได้เซิร์ฟเวอร์ sql ส่งวัตถุเพียง 10 ชิ้นจากฐานข้อมูลของคุณ (ไม่ใช่ทั้งหมดพันล้าน URL ที่เก็บไว้ในฐานข้อมูล)

เอาล่ะมาดูโค้ดแรกกัน:

public async Task<IQueryable<URL>> GetAllUrlsAsync()
{
    var urls = await context.Urls.ToListAsync();
    return urls.AsQueryable();
}

ด้วยตัวอย่างการใช้งานเดียวกันเราได้รับ:

  1. เรามีการโหลดในหน่วยความจำทั้งหมดพันล้าน URL await context.Urls.ToListAsync();ที่เก็บไว้ในฐานข้อมูลของคุณโดยใช้
  2. เรามีหน่วยความจำล้น วิธีที่ถูกต้องในการฆ่าเซิร์ฟเวอร์ของคุณ

เกี่ยวกับ async / await

เหตุใดจึงควรใช้ async / await ลองดูรหัสนี้:

var stuff1 = repo.GetStuff1ForUser(userId);
var stuff2 = repo.GetStuff2ForUser(userId);
return View(new Model(stuff1, stuff2));

เกิดอะไรขึ้นที่นี่?

  1. เริ่มต้นที่บรรทัดที่ 1 var stuff1 = ...
  2. เราส่งคำขอไปยังเซิร์ฟเวอร์ sql ที่เราต้องการได้รับสิ่งที่ต้องการ userId
  3. เรารอ (เธรดปัจจุบันถูกบล็อก)
  4. เรารอ (เธรดปัจจุบันถูกบล็อก)
  5. .....
  6. เซิร์ฟเวอร์ SQL ส่งการตอบสนองถึงเรา
  7. เราย้ายไปที่บรรทัดที่ 2 var stuff2 = ...
  8. เราส่งคำขอไปยังเซิร์ฟเวอร์ sql ที่เราต้องการรับ stuff2 สำหรับ userId
  9. เรารอ (เธรดปัจจุบันถูกบล็อก)
  10. และอีกครั้ง
  11. .....
  12. เซิร์ฟเวอร์ SQL ส่งการตอบสนองถึงเรา
  13. เราแสดงมุมมอง

ลองดูเวอร์ชัน async ของมัน:

var stuff1Task = repo.GetStuff1ForUserAsync(userId);
var stuff2Task = repo.GetStuff2ForUserAsync(userId);
await Task.WhenAll(stuff1Task, stuff2Task);
return View(new Model(stuff1Task.Result, stuff2Task.Result));

เกิดอะไรขึ้นที่นี่?

  1. เราส่งคำขอไปยังเซิร์ฟเวอร์ sql เพื่อรับ stuff1 (บรรทัดที่ 1)
  2. เราส่งคำขอไปยังเซิร์ฟเวอร์ sql เพื่อรับ stuff2 (บรรทัดที่ 2)
  3. เรารอการตอบกลับจากเซิร์ฟเวอร์ sql แต่เธรดปัจจุบันไม่ถูกบล็อกเขาสามารถจัดการการสืบค้นจากผู้ใช้รายอื่นได้
  4. เราแสดงมุมมอง

ทำถูกวิธี

รหัสที่ดีที่นี่:

using System.Data.Entity;

public IQueryable<URL> GetAllUrls()
{
   return context.Urls.AsQueryable();
}

public async Task<List<URL>> GetAllUrlsByUser(int userId) {
   return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();
}

โปรดทราบว่าคุณต้องเพิ่มusing System.Data.Entityเพื่อใช้วิธีการToListAsync()IQueryable

IQueryableหมายเหตุว่าถ้าคุณไม่จำเป็นต้องกรองและเพจและสิ่งที่คุณไม่จำเป็นต้องทำงานร่วมกับ คุณสามารถใช้await context.Urls.ToListAsync()และทำงานกับรูปธรรมList<Url>ได้


3
@Korijn ดูรูปภาพi2.iis.net/media/7188126/…จากสถาปัตยกรรม Introduction to IISฉันสามารถพูดได้ว่าคำขอทั้งหมดใน IIS ได้รับการประมวลผลแบบอะซิงโครนัส
Viktor Lova

7
เนื่องจากคุณไม่ได้ดำเนินการกับผลลัพธ์ที่ตั้งไว้ในไฟล์ GetAllUrlsByUserเมธอดคุณจึงไม่จำเป็นต้องทำให้เป็น async เพียงแค่คืนงานและบันทึกเครื่องสถานะที่ไม่จำเป็นของคุณเองจากการสร้างโดยคอมไพเลอร์
Johnathon Sullinger

1
@JohnathonSullinger แม้ว่าจะทำงานได้อย่างมีความสุข แต่ก็ไม่มีผลข้างเคียงที่ข้อยกเว้นใด ๆ จะไม่ปรากฏที่นี่และเผยแพร่ไปยังที่แรกที่รอคอย? (ไม่ใช่ว่าไม่ดี แต่เป็นการเปลี่ยนแปลงพฤติกรรม?)
Henry Been

9
สิ่งที่น่าสนใจไม่มีใครสังเกตว่าตัวอย่างโค้ดที่ 2 ใน "About async / await" นั้นไร้ความรู้สึกโดยสิ้นเชิงเพราะมันจะทำให้เกิดข้อยกเว้นเนื่องจากทั้ง EF และ EF Core ไม่มีเธรดที่ปลอดภัยดังนั้นการพยายามรันแบบขนานจะทำให้เกิดข้อยกเว้น
Tseng

1
แม้ว่าคำตอบนี้จะถูกต้อง แต่ฉันขอแนะนำให้หลีกเลี่ยงการใช้asyncและawaitหากคุณไม่ได้ทำอะไรกับรายการนี้ ให้โทรไปที่awaitมัน เมื่อคุณรอการโทรในขั้นตอนนี้return await GetAllUrls().Where(u => u.User.Id == userId).ToListAsync();คุณกำลังสร้าง async wrapper พิเศษเมื่อคุณแยกส่วนประกอบและดูที่ IL
Ali Khakpouri

10

มีความแตกต่างอย่างมากในตัวอย่างที่คุณโพสต์เวอร์ชันแรก:

var urls = await context.Urls.ToListAsync();

สิ่งนี้ไม่ดีโดยทั่วไปจะselect * from tableส่งคืนผลลัพธ์ทั้งหมดไปยังหน่วยความจำแล้วนำไปใช้whereกับสิ่งนั้นในการรวบรวมหน่วยความจำแทนที่จะทำselect * from table where...กับฐานข้อมูล

วิธีที่สองจะไม่ตีฐานข้อมูลจริงจนกว่าจะมีการใช้แบบสอบถามกับIQueryable(อาจใช้.Where().Select()การดำเนินการสไตล์linq ซึ่งจะส่งคืนเฉพาะค่า db ที่ตรงกับแบบสอบถามเท่านั้น

หากตัวอย่างของคุณเทียบเคียงได้asyncเวอร์ชันมักจะช้าลงเล็กน้อยต่อคำขอเนื่องจากมีค่าใช้จ่ายมากกว่าในเครื่องสถานะที่คอมไพเลอร์สร้างขึ้นเพื่อให้สามารถใช้asyncงานได้

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


7
จนกว่าแบบสอบถามจะถูกนำไปใช้กับ IQueryable .... ไม่มีทั้ง IQueryable ที่ไหนและ IQueryable เลือกบังคับให้แบบสอบถามดำเนินการ ก่อนหน้านี้ใช้เพรดิเคตและหลังใช้การคาดคะเน จะไม่ดำเนินการจนกว่าจะใช้ตัวดำเนินการที่เป็นรูปธรรมเช่น ToList, ToArray, Single หรือ First
JJS

0

เรื่องสั้นขนาดยาว
IQueryableได้รับการออกแบบมาเพื่อเลื่อนกระบวนการ RUN และขั้นแรกสร้างนิพจน์ร่วมกับIQueryableนิพจน์อื่น ๆจากนั้นตีความและรันนิพจน์โดยรวม
แต่ToList()วิธีการ (หรือวิธีการบางอย่างเช่นนั้น) เป็นที่กล่าวถึงในการเรียกใช้นิพจน์ทันที "ตามสภาพ"
วิธีแรกของคุณ ( GetAllUrlsAsync) จะทำงานทันทีเนื่องจากIQueryableตามด้วยToListAsync()วิธีการ ดังนั้นมันจึงทำงานทันที (แบบอะซิงโครนัส) และส่งคืนจำนวนIEnumerables
ในขณะเดียวกันวิธีที่สองของคุณ ( GetAllUrls) จะไม่ทำงาน แต่จะส่งคืนนิพจน์และ CALLER ของเมธอดนี้รับผิดชอบในการรันนิพจน์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.