การห่อรหัสซิงโครนัสเป็นการโทรแบบอะซิงโครนัส


97

ฉันมีวิธีการในแอปพลิเคชัน ASP.NET ซึ่งใช้เวลาค่อนข้างมากในการดำเนินการ การเรียกใช้เมธอดนี้อาจเกิดขึ้นได้สูงสุด 3 ครั้งระหว่างคำขอของผู้ใช้หนึ่งครั้งขึ้นอยู่กับสถานะแคชและพารามิเตอร์ที่ผู้ใช้ระบุ การโทรแต่ละครั้งจะใช้เวลาประมาณ 1-2 วินาทีจึงจะเสร็จสมบูรณ์ วิธีนี้เป็นการโทรแบบซิงโครนัสไปยังบริการและไม่มีความเป็นไปได้ที่จะลบล้างการใช้งาน
ดังนั้นการโทรแบบซิงโครนัสไปยังบริการจึงมีลักษณะดังนี้:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

และการใช้วิธีการคือ (โปรดทราบว่าการเรียกใช้เมธอดนั้นอาจเกิดขึ้นมากกว่าหนึ่งครั้ง):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

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

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

การใช้งาน (ส่วนหนึ่งของโค้ด "ทำอย่างอื่น" ทำงานพร้อมกันกับการเรียกใช้บริการ):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

คำถามของฉันมีดังต่อไปนี้ ฉันใช้แนวทางที่ถูกต้องเพื่อเร่งการดำเนินการในแอปพลิเคชัน ASP.NET หรือฉันกำลังทำงานที่ไม่จำเป็นพยายามเรียกใช้รหัสซิงโครนัสแบบอะซิงโครนัส ใครสามารถอธิบายได้ว่าทำไมแนวทางที่สองจึงไม่ใช่ตัวเลือกใน ASP.NET (ถ้าไม่ใช่จริงๆ) นอกจากนี้หากใช้แนวทางดังกล่าวได้ฉันจำเป็นต้องเรียกใช้วิธีการดังกล่าวแบบอะซิงโครนัสหรือไม่หากเป็นการโทรเพียงครั้งเดียวที่เราอาจดำเนินการในขณะนี้ (ฉันมีกรณีเช่นนี้เมื่อไม่มีสิ่งอื่นที่ต้องทำในขณะที่รอให้เสร็จสิ้น)
บทความส่วนใหญ่ในเน็ตในหัวข้อนี้ครอบคลุมถึงการใช้async-awaitวิธีการกับโค้ดซึ่งมีawaitableวิธีการอยู่แล้วแต่นั่นไม่ใช่กรณีของฉัน ที่นี่เป็นบทความที่ดีที่อธิบายกรณีของฉันซึ่งไม่ได้อธิบายถึงสถานการณ์ของการโทรแบบขนานการปฏิเสธตัวเลือกในการตัดการโทรแบบซิงค์ แต่ในความคิดของฉันสถานการณ์ของฉันเป็นไปตามโอกาสที่จะทำ
ขอบคุณล่วงหน้าสำหรับความช่วยเหลือและเคล็ดลับ

คำตอบ:


119

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

สิ่งแรกที่ต้องทำคือประเมินสมมติฐานนี้ใหม่:

วิธีนี้เป็นการโทรแบบซิงโครนัสไปยังบริการและไม่มีความเป็นไปได้ที่จะแทนที่การใช้งาน

หาก "บริการ" ของคุณเป็นบริการบนเว็บหรือสิ่งอื่นใดที่เชื่อมโยงกับ I / O ทางออกที่ดีที่สุดคือการเขียน API แบบอะซิงโครนัสสำหรับบริการนั้น

ฉันจะดำเนินการต่อโดยสมมติว่า "บริการ" ของคุณเป็นการดำเนินการที่เชื่อมต่อกับ CPU ซึ่งต้องดำเนินการบนเครื่องเดียวกับเว็บเซิร์ฟเวอร์

หากเป็นเช่นนั้นสิ่งต่อไปที่ต้องประเมินคือสมมติฐานอื่น:

ฉันต้องการคำขอเพื่อดำเนินการเร็วขึ้น

คุณแน่ใจจริงๆหรือว่านั่นคือสิ่งที่คุณต้องทำ? มีการเปลี่ยนแปลงส่วนหน้าที่คุณสามารถดำเนินการแทนได้หรือไม่เช่นเริ่มต้นคำขอและอนุญาตให้ผู้ใช้ทำงานอื่น ๆ ในขณะที่กำลังประมวลผล

ฉันจะดำเนินการต่อโดยสันนิษฐานว่าใช่คุณจำเป็นต้องทำให้คำขอแต่ละรายการดำเนินการเร็วขึ้น

ในกรณีนี้คุณจะต้องรันโค้ดคู่ขนานบนเว็บเซิร์ฟเวอร์ของคุณ สิ่งนี้ไม่แนะนำโดยทั่วไปอย่างแน่นอนที่สุดเนื่องจากรหัสคู่ขนานจะใช้เธรดที่ ASP.NET อาจต้องจัดการกับคำขออื่น ๆ และการลบ / เพิ่มเธรดจะทำให้ ASP.NET threadpool heuristics ปิด ดังนั้นการตัดสินใจนี้จะส่งผลกระทบต่อเซิร์ฟเวอร์ทั้งหมดของคุณ

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

ดังนั้นหากคุณมาไกลและแน่ใจว่าต้องการทำการประมวลผลแบบขนานบน ASP.NET แสดงว่าคุณมีสองตัวเลือก

วิธีหนึ่งที่ง่ายกว่าคือการใช้งานTask.Runซึ่งคล้ายกับรหัสที่คุณมีอยู่ อย่างไรก็ตามฉันไม่แนะนำให้ใช้CalculateAsyncวิธีการเนื่องจากหมายความว่าการประมวลผลเป็นแบบอะซิงโครนัส (ซึ่งไม่ใช่) ให้ใช้Task.Runที่จุดโทรแทน:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

หรือถ้ามันทำงานได้ดีกับรหัสของคุณคุณสามารถใช้Parallelชนิดเช่นParallel.For, หรือParallel.ForEach Parallel.Invokeข้อได้เปรียบของParallelรหัสคือเธรดคำขอถูกใช้เป็นหนึ่งในเธรดคู่ขนานจากนั้นจึงดำเนินการต่อในบริบทเธรด (มีการสลับบริบทน้อยกว่าasyncตัวอย่าง):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

ฉันไม่แนะนำให้ใช้ Parallel LINQ (PLINQ) บน ASP.NET เลย


1
ขอบคุณมากสำหรับคำตอบโดยละเอียด อีกสิ่งหนึ่งที่ฉันอยากจะชี้แจง เมื่อฉันเริ่มดำเนินการโดยใช้Task.Runเนื้อหาจะถูกรันในเธรดอื่นซึ่งนำมาจากเธรดพูล จากนั้นไม่จำเป็นต้องรวมสายที่ปิดกั้น (เช่นการโทรไปยังบริการของฉัน) ลงในRuns เพราะพวกเขาจะใช้เธรดหนึ่งเธรดเสมอซึ่งจะถูกบล็อกระหว่างการดำเนินการของวิธีการ ในสถานการณ์เช่นนี้ผลประโยชน์เดียวที่เหลือasync-awaitอยู่ในกรณีของฉันคือการดำเนินการหลายอย่างพร้อมกัน กรุณาแก้ไขฉันถ้าฉันผิด
Eadel

2
ใช่ประโยชน์เดียวที่คุณจะได้รับจากRun(หรือParallel) คือการเกิดขึ้นพร้อมกัน การดำเนินการแต่ละครั้งยังคงบล็อกเธรด เนื่องจากคุณบอกว่าบริการนี้เป็นบริการบนเว็บฉันจึงไม่แนะนำให้ใช้RunหรือParallel; ให้เขียน API แบบอะซิงโครนัสสำหรับบริการแทน
Stephen Cleary

3
@StephenCleary. คุณหมายถึงอะไรโดย "... และเนื่องจากแต่ละการดำเนินการเป็นแบบอะซิงโครนัสจึงไม่มีการใช้เธรด ... " ขอบคุณ!
alltej

3
@alltej: สำหรับรหัสตรงกันอย่างแท้จริงมีด้ายไม่มี
Stephen Cleary

4
@JoshuaFrank: ไม่โดยตรง โค้ดหลัง API แบบซิงโครนัสต้องบล็อกเธรดการเรียกตามคำจำกัดความ คุณสามารถใช้ API แบบอะซิงโครนัสเช่นBeginGetResponseและเริ่มต้นหลายรายการจากนั้นบล็อก (ซิงโครนัส) จนกว่าทั้งหมดจะเสร็จสมบูรณ์ แต่วิธีการแบบนี้เป็นเรื่องยาก - ดูblog.stephencleary.com/2012/07/dont-block-on -async-code.htmlและmsdn.microsoft.com/en-us/magazine/mt238404.aspx โดยปกติจะง่ายกว่าและสะอาดasyncกว่าหากเป็นไปได้
Stephen Cleary

1

ฉันพบว่าโค้ดต่อไปนี้สามารถแปลงงานให้ทำงานแบบอะซิงโครนัสได้เสมอ

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

และฉันได้ใช้มันในลักษณะต่อไปนี้

await ForceAsync(() => AsyncTaskWithNoAwaits())

การดำเนินการนี้จะดำเนินการงานใด ๆ แบบอะซิงโครนัสเพื่อให้คุณสามารถรวมเข้าด้วยกันในสถานการณ์เมื่อทุกสถานการณ์และการใช้งานอื่น ๆ

คุณสามารถเพิ่ม Task.Yield () เป็นบรรทัดแรกของรหัสที่คุณเรียก

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