ใช้ async / await อย่างมีประสิทธิภาพด้วย ASP.NET Web API


114

ฉันพยายามใช้async/awaitคุณสมบัติของ ASP.NET ในโครงการ Web API ของฉัน ฉันไม่แน่ใจมากนักว่าจะสร้างความแตกต่างในประสิทธิภาพของบริการ Web API ของฉันหรือไม่ โปรดดูขั้นตอนการทำงานและโค้ดตัวอย่างด้านล่างจากแอปพลิเคชันของฉัน

ขั้นตอนการทำงาน:

แอปพลิเคชัน UI →ปลายทาง Web API (ตัวควบคุม) →วิธีการโทรในชั้นบริการ Web API →เรียกใช้บริการเว็บภายนอกอื่น (ที่นี่เรามีการโต้ตอบฐานข้อมูล ฯลฯ )

ควบคุม:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

ชั้นบริการ:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

ฉันทดสอบโค้ดด้านบนและใช้งานได้ แต่ผมไม่แน่ใจว่าเป็นการใช้งานasync/awaitไฟล์. กรุณาแบ่งปันความคิดของคุณ


คำตอบ:


203

ฉันไม่แน่ใจมากว่ามันจะสร้างความแตกต่างในประสิทธิภาพของ API ของฉันหรือไม่

โปรดทราบว่าประโยชน์หลักของรหัสอะซิงโครนัสทางฝั่งเซิร์ฟเวอร์คือความสามารถในการปรับขนาดได้ จะไม่ทำให้คำขอของคุณทำงานเร็วขึ้นอย่างน่าอัศจรรย์ ฉันครอบคลุมหลาย "ฉันควรใช้async" การพิจารณาในของฉันบทความเกี่ยวกับasync ASP.NET

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

เท่าที่โค้ดไปนี่ไม่ใช่แบบอะซิงโครนัส:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

คุณต้องมีการใช้งานแบบอะซิงโครนัสอย่างแท้จริงเพื่อให้ได้ประโยชน์จากasync:

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

หรือ (ถ้าตรรกะของคุณในวิธีนี้เป็นเพียงการส่งผ่าน):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

โปรดทราบว่าการทำงานจาก "ภายในสู่ภายนอก" นั้นง่ายกว่าการทำงานแบบนี้ กล่าวอีกนัยหนึ่งอย่าเริ่มต้นด้วยการดำเนินการควบคุมแบบอะซิงโครนัสจากนั้นบังคับให้วิธีการดาวน์สตรีมเป็นแบบอะซิงโครนัส ให้ระบุการดำเนินการแบบอะซิงโครนัสตามธรรมชาติแทน (การเรียก API ภายนอกแบบสอบถามฐานข้อมูล ฯลฯ ) และกำหนดให้อะซิงโครนัสที่ระดับต่ำสุดก่อน ( Service.ProcessAsync) จากนั้นปล่อยให้asyncหยดขึ้นทำให้การทำงานของคอนโทรลเลอร์ของคุณไม่ตรงกันเป็นขั้นตอนสุดท้าย

และคุณไม่ควรใช้Task.Runในสถานการณ์นี้


4
ขอบคุณ Stephen สำหรับความคิดเห็นที่มีค่าของคุณ ฉันเปลี่ยนวิธีการชั้นบริการทั้งหมดของฉันเป็น async และตอนนี้ฉันเรียกการเรียก REST ภายนอกโดยใช้เมธอด ExecuteTaskAsync และทำงานตามที่คาดไว้ ขอขอบคุณสำหรับบทความในบล็อกของคุณเกี่ยวกับ async และ Tasks มันช่วยฉันมากในการทำความเข้าใจเบื้องต้น
arp

5
คุณช่วยอธิบายได้ไหมว่าทำไมคุณถึงไม่ใช้ Task Run ที่นี่
Maarten

1
@Maarten: ผมอธิบายมัน (สั้น ๆ ) ในบทความที่ผมเชื่อมโยงกับ ผมเข้าไปดูรายละเอียดในบล็อกของฉัน
Stephen Cleary

ฉันได้อ่านบทความของคุณ Stephen และดูเหมือนว่าจะหลีกเลี่ยงการกล่าวถึงบางสิ่ง เมื่อคำขอ ASP.NET มาถึงและเริ่มต้นการทำงานบนเธรดพูลเธรดก็ไม่เป็นไร แต่ถ้ามันกลายเป็น async ใช่มันจะเริ่มการประมวลผล async และเธรดนั้นจะกลับไปที่พูลทันที แต่งานที่ดำเนินการในวิธี async จะดำเนินการกับเธรดพูลเธรดเอง!
Hugh

@Hugh: Asynchronous I / O การดำเนินงานไม่ปิดกั้นด้าย
Stephen Cleary

12

ถูกต้อง แต่อาจไม่มีประโยชน์

เนื่องจากไม่มีอะไรต้องรอ - ไม่มีการเรียกร้องให้บล็อก API ซึ่งสามารถทำงานแบบอะซิงโครนัสได้ดังนั้นคุณจึงตั้งค่าโครงสร้างเพื่อติดตามการทำงานแบบอะซิงโครนัส (ซึ่งมีค่าโสหุ้ย) แต่ไม่ได้ใช้ประโยชน์จากความสามารถนั้น

ตัวอย่างเช่นถ้าชั้นบริการกำลังดำเนินการกับ DB กับ Entity Framework ซึ่งสนับสนุนการเรียกแบบอะซิงโครนัส:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

คุณอนุญาตให้เธรดผู้ปฏิบัติงานทำอย่างอื่นได้ในขณะที่กำลังสอบถามฐานข้อมูล (และสามารถดำเนินการตามคำขออื่นได้)

การรอคอยมีแนวโน้มที่จะเป็นสิ่งที่ต้องดำเนินการไปตลอดทาง: เป็นการยากมากที่จะย้อนยุคเข้ากับระบบที่มีอยู่


-1

คุณไม่ได้ใช้ประโยชน์จาก async / await อย่างมีประสิทธิภาพเนื่องจากเธรดการร้องขอจะถูกบล็อกขณะเรียกใช้วิธีซิงโครนัสReturnAllCountries()

เธรดที่ได้รับมอบหมายให้จัดการกับคำร้องขอจะรออยู่เฉยๆในขณะที่ReturnAllCountries()ทำงาน

หากคุณสามารถนำReturnAllCountries()ไปใช้เป็นแบบอะซิงโครนัสได้คุณจะเห็นประโยชน์ในการปรับขนาดได้ เนื่องจากเธรดสามารถถูกปล่อยกลับไปที่เธรดพูล. NET เพื่อจัดการกับคำร้องขออื่นในขณะที่ReturnAllCountries()กำลังดำเนินการ สิ่งนี้จะช่วยให้บริการของคุณมีปริมาณงานที่สูงขึ้นโดยใช้เธรดได้อย่างมีประสิทธิภาพมากขึ้น


นี่ไม่เป็นความจริง. ซ็อกเก็ตเชื่อมต่ออยู่ แต่เธรดที่ประมวลผลคำขอสามารถทำอย่างอื่นได้เช่นประมวลผลคำขออื่นในขณะที่รอสายบริการ
Garr Godfrey

-8

ฉันจะเปลี่ยนชั้นบริการของคุณเป็น:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

ตามที่คุณมีคุณยังคง_service.Processโทรออกพร้อมกันและได้รับประโยชน์น้อยมากจากการรอคอย

ด้วยวิธีนี้คุณกำลังตัดสายที่อาจช้าลงใน a Taskเริ่มต้นและส่งคืนเพื่อรอ ตอนนี้คุณจะได้รับประโยชน์จากการรอไฟล์Task.


3
ตามลิงค์ของบล็อกฉันเห็นว่าแม้ว่าเราจะใช้งานก็ตามการเรียกใช้วิธีนี้ยังคงทำงานแบบซิงโครนัสหรือไม่
arp

อืมมม มีความแตกต่างมากมายระหว่างโค้ดด้านบนและลิงก์นั้น ของคุณServiceไม่ใช่วิธี async และจะไม่ถูกรอภายในการServiceเรียกใช้ ฉันเข้าใจประเด็นของเขา แต่ฉันไม่เชื่อว่ามันใช้ได้กับที่นี่
Jonesopolis

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