เหตุใดการกระทำ async จึงหยุดทำงาน


103

ฉันมีหลายชั้นสุทธิ 4.5 โปรแกรมประยุกต์ที่เรียกวิธีการใช้ C # 's ใหม่asyncและawaitคำหลักที่เพิ่งแฮงค์และผมก็ไม่สามารถดูว่าทำไม

ที่ด้านล่างฉันมีวิธีการ async ที่ขยายยูทิลิตี้ฐานข้อมูลของเราOurDBConn(โดยทั่วไปจะเป็น wrapper สำหรับพื้นฐานDBConnectionและDBCommandวัตถุ):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

จากนั้นฉันมีวิธี async ระดับกลางที่เรียกสิ่งนี้เพื่อรับผลรวมที่ทำงานช้า:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

ในที่สุดฉันก็มีวิธี UI (การกระทำ MVC) ที่ทำงานพร้อมกัน:

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

ปัญหาคือมันค้างอยู่ที่บรรทัดสุดท้ายตลอดไป มันก็เหมือนกันถ้าฉันโทรasyncTask.Wait(). ถ้าฉันเรียกใช้เมธอด SQL ที่ช้าโดยตรงจะใช้เวลาประมาณ 4 วินาที

พฤติกรรมที่ฉันคาดหวังคือเมื่อได้รับasyncTask.Resultหากยังไม่เสร็จสิ้นควรรอจนกว่าจะเป็นและเมื่อเป็นเช่นนั้นควรส่งคืนผลลัพธ์

ถ้าฉันก้าวผ่านดีบักเกอร์คำสั่ง SQL จะเสร็จสมบูรณ์และฟังก์ชันแลมบ์ดาจะเสร็จสิ้น แต่ไม่ถึงreturn result;บรรทัดของGetTotalAsync

มีความคิดว่าฉันทำอะไรผิดหรือเปล่า?

ข้อเสนอแนะใด ๆ ที่ฉันต้องตรวจสอบเพื่อแก้ไขปัญหานี้หรือไม่

นี่อาจเป็นการหยุดชะงักที่ไหนสักแห่งและถ้าเป็นเช่นนั้นมีวิธีใดที่จะพบได้โดยตรง

คำตอบ:


150

ใช่นั่นเป็นทางตันเลย และข้อผิดพลาดทั่วไปกับ TPL ดังนั้นอย่ารู้สึกแย่

เมื่อคุณเขียนawait fooรันไทม์โดยค่าเริ่มต้นจะกำหนดเวลาความต่อเนื่องของฟังก์ชันบน SynchronizationContext เดียวกับที่เมธอดเริ่มทำงาน ในภาษาอังกฤษสมมติว่าคุณเรียกคุณExecuteAsyncจากเธรด UI คำค้นหาของคุณทำงานบนเธรดพูลเธรด (เนื่องจากคุณเรียกTask.Run) แต่คุณรอผลลัพธ์ ซึ่งหมายความว่ารันไทม์จะกำหนดเวลาให้return result;บรรทัด " " ของคุณทำงานกลับบนเธรด UI แทนที่จะกำหนดเวลากลับไปที่เธรดพูล

แล้วการหยุดชะงักนี้เป็นอย่างไร? ลองนึกภาพคุณมีรหัสนี้:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

ดังนั้นบรรทัดแรกจึงเริ่มต้นการทำงานแบบอะซิงโครนัส บรรทัดที่สองแล้วบล็อกหัวข้อ UI ดังนั้นเมื่อรันไทม์ต้องการรันบรรทัด "return result" กลับบนเธรด UI ก็ไม่สามารถทำได้จนกว่าจะResultเสร็จสมบูรณ์ แต่แน่นอนว่าไม่สามารถให้ผลลัพธ์ได้จนกว่าผลตอบแทนจะเกิดขึ้น การหยุดชะงัก

สิ่งนี้แสดงให้เห็นถึงกฎสำคัญของการใช้ TPL: เมื่อคุณใช้.Resultกับเธรด UI (หรือบริบทการซิงค์แฟนซีอื่น ๆ ) คุณต้องระมัดระวังเพื่อให้แน่ใจว่าไม่มีสิ่งใดที่ขึ้นอยู่กับงานที่กำหนดไว้ในเธรด UI หรือความชั่วร้ายเกิดขึ้น

แล้วคุณจะทำอย่างไร? ตัวเลือก # 1 กำลังรออยู่ทุกที่ แต่อย่างที่คุณบอกว่านั่นไม่ใช่ตัวเลือก ตัวเลือกที่สองที่คุณสามารถใช้ได้คือหยุดใช้ await คุณสามารถเขียนฟังก์ชันทั้งสองใหม่เป็น:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

อะไรคือความแตกต่าง? ขณะนี้ไม่มีการรอคอยใด ๆ ดังนั้นจึงไม่มีการกำหนดเวลาไว้ในเธรด UI โดยปริยาย สำหรับวิธีง่ายๆเช่นนี้ที่มีผลตอบแทนเพียงครั้งเดียวไม่มีประเด็นที่จะทำvar result = await...; return resultรูปแบบ ""; เพียงแค่ลบตัวปรับเปลี่ยน async และส่งวัตถุงานไปรอบ ๆ โดยตรง ค่าใช้จ่ายน้อยกว่าถ้าไม่มีอะไรอื่น

ตัวเลือก # 3 คือการระบุว่าคุณไม่ต้องการให้การรอคอยของคุณกำหนดเวลากลับไปที่เธรด UI แต่เพียงแค่กำหนดเวลาไปที่เธรดพูล คุณทำสิ่งนี้ด้วยConfigureAwaitวิธีการดังนี้:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

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


6
BTW คำถามเกี่ยวกับ ASP.NET ดังนั้นจึงไม่มีเธรด UI แต่ปัญหาเกี่ยวกับการหยุดชะงักนั้นเหมือนกันSynchronizationContextทุกประการเนื่องจาก ASP.NET
svick

นั่นอธิบายได้มากเนื่องจากฉันมีรหัส. Net 4 ที่คล้ายกันซึ่งไม่มีปัญหา แต่ใช้ TPL โดยไม่มีasync/ awaitคำหลัก
Keith


หากใครกำลังมองหาโค้ด VB.net (เหมือนผม) อธิบายได้ที่นี่: docs.microsoft.com/en-us/dotnet/visual-basic/programming-guide/…
MichaelDarkBlue

คุณช่วยฉันได้ไหมในstackoverflow.com/questions/54360300/…
Jitendra Pancholi

36

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

มีสองแนวทางที่จะหลีกเลี่ยงการหยุดชะงักนี้:

  1. ใช้asyncวิธีลงให้หมด คุณบอกว่าคุณ "ทำไม่ได้" แต่ฉันไม่แน่ใจว่าทำไมไม่ทำ ASP.NET MVC บน. NET 4.5 สามารถรองรับasyncการกระทำได้อย่างแน่นอนและไม่ใช่เรื่องยากที่จะทำการเปลี่ยนแปลง
  2. ใช้ConfigureAwait(continueOnCapturedContext: false)ให้มากที่สุด สิ่งนี้จะลบล้างพฤติกรรมเริ่มต้นของการดำเนินการต่อในบริบทที่จับ

ไม่ConfigureAwait(false)รับประกันว่าฟังก์ชั่นในปัจจุบันการดำเนินการต่อในบริบทที่แตกต่างกันอย่างไร
chue x

เฟรมเวิร์ก MVC รองรับ แต่นี่เป็นส่วนหนึ่งของแอพ MVC ที่มีอยู่ซึ่งมี JS ฝั่งไคลเอ็นต์จำนวนมากอยู่แล้ว ฉันไม่สามารถเปลี่ยนไปใช้asyncการกระทำได้อย่างง่ายดายโดยไม่ทำลายวิธีการทำงานของฝั่งไคลเอ็นต์ ฉันวางแผนที่จะตรวจสอบตัวเลือกนั้นในระยะยาวอย่างแน่นอน
Keith

เพียงเพื่อชี้แจงความคิดเห็นของฉัน - ฉันอยากรู้ว่าการใช้ConfigureAwait(false)call tree จะช่วยแก้ปัญหาของ OP ได้หรือไม่
chue x

3
@Keith: การทำ MVC asyncไม่ส่งผลกระทบต่อฝั่งไคลเอ็นต์เลย ผมอธิบายเรื่องนี้ในโพสต์บล็อกอื่นไม่เปลี่ยนแปลงโปรโตคอลasync HTTP
Stephen Cleary

1
@Keith: เป็นเรื่องปกติที่asyncจะ "เติบโต" ผ่านโค้ดเบส หากวิธีการควบคุมของคุณอาจขึ้นอยู่กับการดำเนินการแบบอะซิงโครนัสเมธอดคลาสพื้นฐานควรจะคืนTask<ActionResult>ค่า การเปลี่ยนโปรเจ็กต์ขนาดใหญ่asyncเป็นเรื่องที่น่าอึดอัดใจอยู่เสมอเนื่องจากการผสมasyncและซิงค์โค้ดเป็นเรื่องยากและยุ่งยาก asyncรหัสบริสุทธิ์นั้นง่ายกว่ามาก
Stephen Cleary

12

ฉันอยู่ในสถานการณ์ชะงักงันเดียวกัน แต่ในกรณีของฉันเรียกวิธีการ async จากวิธีการซิงค์สิ่งที่เหมาะกับฉันคือ:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

นี่เป็นแนวทางที่ดีมีความคิดอย่างไร


วิธีนี้ใช้ได้ผลสำหรับฉันเช่นกัน แต่ฉันไม่แน่ใจว่ามันเป็นวิธีแก้ปัญหาที่ดีหรืออาจพังที่ไหนสักแห่ง ใครก็ได้สามารถอธิบายได้
Konstantin Vdovkin

ในที่สุดฉันก็ใช้วิธีแก้ปัญหานี้และทำงานในสภาพแวดล้อมที่มีประสิทธิผลโดยไม่มีปัญหา .....
Danilow

1
ฉันคิดว่าคุณกำลังประสบความสำเร็จโดยใช้ Task.Run ในงานทดสอบของฉัน Run ใช้เวลาดำเนินการเกือบสองเท่าสำหรับคำขอ http 100ms
Timothy Gonzalez

1
ที่สมเหตุสมผลคุณกำลังสร้างงานใหม่สำหรับการตัดการโทรแบบ async ประสิทธิภาพคือการแลกเปลี่ยน
Danilow

มันยอดเยี่ยมสำหรับฉันเช่นกันกรณีของฉันยังเกิดจากวิธีการซิงโครนัสที่เรียกใช้แบบอะซิงโครนัส ขอบคุณ!
Leonardo Spina

4

เพียงเพื่อเพิ่มคำตอบที่ยอมรับ (ตัวแทนไม่เพียงพอที่จะแสดงความคิดเห็น) ฉันมีปัญหานี้เกิดขึ้นเมื่อมีการบล็อกโดยใช้task.Resultเหตุการณ์แม้ว่าทุกอย่างawaitด้านล่างจะมีConfigureAwait(false)ดังตัวอย่างนี้:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

ปัญหาเกิดจากรหัสไลบรารีภายนอก วิธีการ async ไลบรารีพยายามดำเนินการต่อในบริบทการซิงค์การโทรไม่ว่าฉันจะกำหนดค่าการรอคอยอย่างไรซึ่งนำไปสู่การหยุดชะงัก

ดังนั้นคำตอบคือม้วนรหัสไลบรารีภายนอกของฉันเองExternalLibraryStringAsyncเพื่อให้มีคุณสมบัติการต่อเนื่องที่ต้องการ


คำตอบผิดสำหรับวัตถุประสงค์ทางประวัติศาสตร์

หลังจากเจ็บปวดและปวดร้าวมากฉันพบวิธีแก้ปัญหาที่ฝังอยู่ในบล็อกโพสต์นี้ (Ctrl-f สำหรับ 'การหยุดชะงัก') มันหมุนรอบใช้แทนการเปลือยtask.ContinueWithtask.Result

ตัวอย่างการหยุดชะงักก่อนหน้านี้:

public Foo GetFooSynchronous()
{
    var foo = new Foo();
    foo.Info = GetInfoAsync.Result;  // often deadlocks in ASP.NET
    return foo;
}

private async Task<string> GetInfoAsync()
{ 
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

หลีกเลี่ยงการหยุดชะงักเช่นนี้:

public Foo GetFooSynchronous
{
    var foo = new Foo();
    GetInfoAsync()  // ContinueWith doesn't run until the task is complete
        .ContinueWith(task => foo.Info = task.Result);
    return foo;
}

private async Task<string> GetInfoAsync
{
    return await ExternalLibraryStringAsync().ConfigureAwait(false);
}

การโหวตลดราคาคืออะไร? วิธีนี้ใช้ได้ผลสำหรับฉัน
Cameron Jeffers

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

อืมใช่ฉันเห็น ดังนั้นฉันควรเปิดเผยวิธีการ "รอจนกว่างานจะเสร็จสิ้น" ที่ใช้การบล็อกด้วยตนเองในขณะที่วนซ้ำ (หรืออะไรทำนองนั้น)? หรือแพ็คบล็อกดังกล่าวลงในGetFooSynchronousวิธีการ?
Cameron Jeffers

1
ถ้าคุณทำมันจะชะงักงัน คุณต้อง async ไปตลอดทางโดยส่งคืน a Taskแทนการปิดกั้น
Servy

น่าเสียดายที่ไม่ใช่ตัวเลือกชั้นเรียนใช้อินเทอร์เฟซแบบซิงโครนัสที่ฉันไม่สามารถเปลี่ยนแปลงได้
Cameron Jeffers

0

คำตอบด่วน: เปลี่ยนบรรทัดนี้

ResultClass slowTotal = asyncTask.Result;

ถึง

ResultClass slowTotal = await asyncTask;

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

คุณยังสามารถลองใช้รหัสด้านล่างหากคุณต้องการใช้ผลลัพธ์

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.