ตัวอย่าง async / await ที่ทำให้เกิดการชะงักงัน


97

ฉันพบแนวทางปฏิบัติที่ดีที่สุดสำหรับการเขียนโปรแกรมแบบอะซิงโครนัสโดยใช้ c # async/ awaitคำหลัก (ฉันเพิ่งเริ่มใช้ c # 5.0)

หนึ่งในคำแนะนำที่ได้รับมีดังต่อไปนี้:

ความเสถียร: รู้บริบทการซิงโครไนซ์ของคุณ

... บริบทการซิงโครไนซ์บางส่วนไม่ได้ reentrant และ single-threaded ซึ่งหมายความว่าสามารถทำงานได้เพียงหน่วยเดียวในบริบทในช่วงเวลาที่กำหนด ตัวอย่างนี้คือเธรด UI ของ Windows หรือบริบทการร้องขอ ASP.NET ในบริบทการซิงโครไนซ์แบบเธรดเดี่ยวเหล่านี้คุณสามารถหยุดชะงักได้โดยง่าย หากคุณวางภารกิจจากบริบทเธรดเดียวให้รองานนั้นในบริบทรหัสรอของคุณอาจบล็อกงานเบื้องหลัง

public ActionResult ActionAsync()
{
    // DEADLOCK: this blocks on the async task
    var data = GetDataAsync().Result;

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

ถ้าฉันพยายามที่จะแยกมันออกด้วยตัวเองเธรดหลักจะปรากฏเป็นเธรดใหม่MyWebService.GetDataAsync();แต่เนื่องจากเธรดหลักรออยู่ที่นั่นจึงรอผลลัพธ์ในGetDataAsync().Result. ในขณะเดียวกันบอกว่าข้อมูลพร้อมแล้ว เหตุใดเธรดหลักจึงไม่ดำเนินการต่อเป็นตรรกะความต่อเนื่องและส่งคืนผลลัพธ์สตริงจากGetDataAsync()?

ใครช่วยอธิบายหน่อยได้ไหมว่าทำไมถึงเกิดการชะงักงันในตัวอย่างด้านบน ฉันไม่รู้เลยว่าปัญหาคืออะไร ...


คุณแน่ใจจริงๆหรือว่า GetDataAsync เสร็จสิ้นแล้ว? หรือมันติดขัดทำให้เกิดการล็อคและไม่หยุดชะงัก?
Andrey

นี่คือตัวอย่างที่ให้มา ตามความเข้าใจของฉันมันควรจะเสร็จสิ้นและมีผลการจัดเรียงบางอย่างพร้อม ...
Dror Weiss

4
ทำไมคุณถึงรองานนี้? คุณควรรอแทนเพราะโดยพื้นฐานแล้วคุณสูญเสียประโยชน์ทั้งหมดของโมเดล async
Toni Petrina

หากต้องการเพิ่มไปยังจุดของ @ ToniPetrina แม้จะไม่มีปัญหาการหยุดชะงักvar data = GetDataAsync().Result;ก็เป็นบรรทัดของรหัสที่ไม่ควรทำในบริบทที่คุณไม่ควรปิดกั้น (คำขอ UI หรือ ASP.NET) แม้ว่าจะไม่เกิดการหยุดชะงัก แต่ก็เป็นการบล็อกเธรดในระยะเวลาที่ไม่แน่นอน โดยพื้นฐานแล้วมันเป็นตัวอย่างที่น่ากลัว [คุณต้องออกจากเธรด UI ก่อนที่จะรันโค้ดเช่นนั้นหรือใช้ที่awaitนั่นตามที่ Toni แนะนำ]
ToolmakerSteve

คำตอบ:


82

ลองดูตัวอย่างนี้ Stephen มีคำตอบที่ชัดเจนสำหรับคุณ:

นี่คือสิ่งที่เกิดขึ้นโดยเริ่มจากเมธอดระดับบนสุด ( Button1_Clickสำหรับ UI / MyController.Getสำหรับ ASP.NET):

  1. การเรียกเมธอดระดับบนสุดGetJsonAsync(ภายในบริบท UI / ASP.NET)

  2. GetJsonAsyncเริ่มการร้องขอ REST โดยการเรียกHttpClient.GetStringAsync(ยังอยู่ในบริบท)

  3. GetStringAsyncส่งคืนค่าที่ไม่เสร็จสมบูรณ์Taskแสดงว่าคำขอ REST ไม่เสร็จสมบูรณ์

  4. GetJsonAsyncรอการTaskกลับมาโดยGetStringAsync. บริบทถูกจับและจะใช้เพื่อรันGetJsonAsyncวิธีการต่อในภายหลัง GetJsonAsyncส่งคืนค่าที่ไม่สมบูรณ์Taskแสดงว่าGetJsonAsyncเมธอดยังไม่เสร็จสมบูรณ์

  5. วิธีการระดับบนสุดพร้อมบล็อกบนกลับโดยTask GetJsonAsyncสิ่งนี้บล็อกเธรดบริบท

  6. ... ในที่สุดคำขอ REST จะเสร็จสมบูรณ์ นี้เสร็จสมบูรณ์ที่ถูกส่งกลับโดยTaskGetStringAsync

  7. GetJsonAsyncตอนนี้ความต่อเนื่องสำหรับพร้อมที่จะรันและรอให้บริบทพร้อมใช้งานเพื่อให้สามารถดำเนินการในบริบทได้

  8. การหยุดชะงัก เมธอดระดับบนสุดกำลังบล็อกเธรดบริบทรอGetJsonAsyncให้เสร็จสมบูรณ์และGetJsonAsyncกำลังรอให้บริบทว่างจึงจะเสร็จสมบูรณ์ สำหรับตัวอย่าง UI "บริบท" คือบริบท UI สำหรับตัวอย่าง ASP.NET "บริบท" คือบริบทการร้องขอ ASP.NET การหยุดชะงักประเภทนี้อาจเกิดจาก "บริบท" อย่างใดอย่างหนึ่ง

ลิงก์อื่นที่คุณควรอ่าน: รอและ UI และการหยุดชะงัก! พุทโธ่!


23
  • ข้อเท็จจริงที่ 1: GetDataAsync().Result;จะทำงานเมื่องานส่งคืนGetDataAsync()เสร็จสมบูรณ์ในระหว่างนี้จะบล็อกเธรด UI
  • ข้อเท็จจริงที่ 2: ความต่อเนื่องของ await ( return result.ToString()) ถูกจัดคิวไว้ที่เธรด UI เพื่อดำเนินการ
  • ข้อเท็จจริงที่ 3: งานที่ส่งคืนโดยGetDataAsync()จะเสร็จสมบูรณ์เมื่อรันการต่อเนื่องในคิว
  • ข้อเท็จจริงที่ 4: การต่อเนื่องในคิวจะไม่ถูกเรียกใช้เนื่องจากเธรด UI ถูกบล็อก (ข้อเท็จจริง 1)

ชะงักงัน!

การหยุดชะงักสามารถทำลายได้โดยทางเลือกอื่นที่ให้ไว้เพื่อหลีกเลี่ยงข้อเท็จจริง 1 หรือข้อเท็จจริง 2

  • หลีกเลี่ยง 1,4. แทนที่จะบล็อกเธรด UI ให้ใช้var data = await GetDataAsync()ซึ่งช่วยให้เธรด UI ทำงานต่อไป
  • หลีกเลี่ยง 2,3 จัดคิวความต่อเนื่องของการรอคอยไปยังเธรดอื่นที่ไม่ถูกบล็อกเช่นการใช้งานvar data = Task.Run(GetDataAsync).Resultซึ่งจะโพสต์ความต่อเนื่องไปยังบริบทการซิงค์ของเธรดพูลเธรด สิ่งนี้ช่วยให้งานที่ส่งคืนโดย GetDataAsync()เสร็จสมบูรณ์

สิ่งนี้อธิบายได้ดีมากในบทความของ Stephen Toubประมาณครึ่งทางที่เขาใช้ตัวอย่างของDelayAsync().


เกี่ยวกับเรื่องนี้var data = Task.Run(GetDataAsync).Resultเป็นเรื่องใหม่สำหรับฉัน ผมคิดเสมอว่าด้านนอก.Resultจะพร้อมใช้งานทันทีที่รอคอยแรกของการGetDataAsyncถูกตีจึงจะเป็นdata defaultน่าสนใจ.
nawfal

19

ฉันเพิ่งเล่นซอกับปัญหานี้อีกครั้งในโครงการ ASP.NET MVC เมื่อคุณต้องการเรียกasyncmethod จาก a PartialViewคุณจะไม่ได้รับอนุญาตให้สร้างไฟล์PartialView async. คุณจะได้รับข้อยกเว้นหากคุณทำ

คุณสามารถใช้วิธีแก้ปัญหาง่ายๆต่อไปนี้ในสถานการณ์ที่คุณต้องการเรียกใช้asyncวิธีการจากวิธีการซิงค์:

  1. ก่อนการโทรให้ล้างไฟล์ SynchronizationContext
  2. โทรออกจะไม่มีการหยุดชะงักอีกต่อไปรอให้เสร็จสิ้น
  3. กู้คืนไฟล์ SynchronizationContext

ตัวอย่าง:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}

3

ประเด็นหลักอีกประการหนึ่งคือคุณไม่ควรปิดกั้น Tasks และใช้ async จนสุดเพื่อป้องกันการชะงักงัน จากนั้นจะเป็นการปิดกั้นแบบอะซิงโครนัสทั้งหมดไม่ใช่ซิงโครนัส

public async Task<ActionResult> ActionAsync()
{

    var data = await GetDataAsync();

    return View(data);
}

private async Task<string> GetDataAsync()
{
    // a very simple async method
    var result = await MyWebService.GetDataAsync();
    return result.ToString();
}

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

ดูคำตอบจาก Philip Ngan ด้านบน (ฉันรู้ว่าสิ่งนี้ถูกโพสต์หลังจากความคิดเห็นนี้): จัดคิวความต่อเนื่องของการรอไปยังเธรดอื่นที่ไม่ถูกบล็อกเช่นใช้ var data = Task.Run (GetDataAsync) ผลลัพธ์
Jeroen

@Dexter - re " จะทำอย่างไรถ้าฉันต้องการให้เธรดหลัก (UI) ถูกบล็อกจนกว่างานจะเสร็จสิ้น " - คุณต้องการให้เธรด UI ถูกบล็อกหรือไม่ซึ่งหมายความว่าผู้ใช้ไม่สามารถทำอะไรได้เลยไม่สามารถยกเลิกได้หรือเป็น คุณไม่ต้องการดำเนินการต่อตามวิธีการที่คุณอยู่? "await" หรือ "Task.ContinueWith" จัดการกรณีหลัง
ToolmakerSteve

@ToolmakerSteve แน่นอนฉันไม่ต้องการดำเนินการต่อไป แต่ฉันไม่สามารถใช้ await ได้เพราะฉันไม่สามารถใช้ async ได้ตลอดทางเช่นกัน - HttpClient ถูกเรียกใช้เป็นหลักซึ่งแน่นอนว่าไม่สามารถ async ได้ จากนั้นฉันก็พูดถึงการทำทั้งหมดนี้ในแอปคอนโซล - ในกรณีนี้ฉันต้องการแบบเดิม - ฉันไม่ต้องการให้แอปของฉันเป็นแบบมัลติเธรด บล็อกทุกอย่าง
Dexter

-1

วิธีแก้ปัญหาที่ฉันได้มาคือการใช้Joinวิธีการขยายในงานก่อนที่จะขอผลลัพธ์

รหัสมีลักษณะดังนี้:

public ActionResult ActionAsync()
{
  var task = GetDataAsync();
  task.Join();
  var data = task.Result;

  return View(data);
}

วิธีการเข้าร่วมคือ:

public static class TaskExtensions
{
    public static void Join(this Task task)
    {
        var currentDispatcher = Dispatcher.CurrentDispatcher;
        while (!task.IsCompleted)
        {
            // Make the dispatcher allow this thread to work on other things
            currentDispatcher.Invoke(delegate { }, DispatcherPriority.SystemIdle);
        }
    }
}

ฉันเข้าสู่โดเมนไม่เพียงพอที่จะเห็นข้อเสียของโซลูชันนี้ (ถ้ามี)

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