HttpClient.GetAsync (…) จะไม่กลับมาเมื่อใช้ await / async


315

แก้ไข: คำถามนี้ดูเหมือนว่าอาจเป็นปัญหาเดียวกัน แต่ไม่มีคำตอบ ...

แก้ไข:ในกรณีทดสอบ 5 งานดูเหมือนจะติดอยู่ในWaitingForActivationสถานะ

ฉันพบพฤติกรรมแปลก ๆ บางอย่างโดยใช้ System.Net.Http.HttpClient ใน. NET 4.5 - ที่ "รอ" ผลการโทรไปที่ (เช่น) httpClient.GetAsync(...)จะไม่กลับมา

สิ่งนี้จะเกิดขึ้นในบางสถานการณ์เมื่อใช้ฟังก์ชันการทำงานของ async / await ใหม่และ Tasks API - รหัสดูเหมือนจะทำงานเมื่อใช้ต่อเนื่องเท่านั้น

นี่คือโค้ดบางส่วนที่ทำให้เกิดปัญหาอีกครั้ง - วางสิ่งนี้ลงใน "MVC 4 WebApi โครงการใหม่" ใน Visual Studio 11 เพื่อแสดงปลายทาง GET ต่อไปนี้:

/api/test1
/api/test2
/api/test3
/api/test4
/api/test5 <--- never completes
/api/test6

จุดปลายแต่ละจุดจะส่งคืนข้อมูลเดียวกัน (ส่วนหัวการตอบสนองจาก stackoverflow.com) ยกเว้น/api/test5ที่ไม่เคยเสร็จสมบูรณ์

ฉันพบข้อผิดพลาดในคลาส HttpClient หรือฉันใช้ API ผิดวิธีหรือไม่

รหัสที่จะทำซ้ำ:

public class BaseApiController : ApiController
{
    /// <summary>
    /// Retrieves data using continuations
    /// </summary>
    protected Task<string> Continuations_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var t = httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return t.ContinueWith(t1 => t1.Result.Content.Headers.ToString());
    }

    /// <summary>
    /// Retrieves data using async/await
    /// </summary>
    protected async Task<string> AsyncAwait_GetSomeDataAsync()
    {
        var httpClient = new HttpClient();

        var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead);

        return result.Content.Headers.ToString();
    }
}

public class Test1Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await Continuations_GetSomeDataAsync();

        return data;
    }
}

public class Test2Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = Continuations_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test3Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return Continuations_GetSomeDataAsync();
    }
}

public class Test4Controller : BaseApiController
{
    /// <summary>
    /// Handles task using Async/Await
    /// </summary>
    public async Task<string> Get()
    {
        var data = await AsyncAwait_GetSomeDataAsync();

        return data;
    }
}

public class Test5Controller : BaseApiController
{
    /// <summary>
    /// Handles task by blocking the thread until the task completes
    /// </summary>
    public string Get()
    {
        var task = AsyncAwait_GetSomeDataAsync();

        var data = task.GetAwaiter().GetResult();

        return data;
    }
}

public class Test6Controller : BaseApiController
{
    /// <summary>
    /// Passes the task back to the controller host
    /// </summary>
    public Task<string> Get()
    {
        return AsyncAwait_GetSomeDataAsync();
    }
}

2
ดูเหมือนจะไม่เป็นปัญหาเดียวกัน แต่เพียงเพื่อให้แน่ใจว่าคุณรู้ว่ามีข้อผิดพลาด MVC4 ในวิธีการเบต้า WRT async ที่เสร็จสมบูรณ์พร้อมกัน - ดูstackoverflow.com/questions/9627329/ …
James Manning

ขอบคุณ - ฉันจะระวังให้ดี ในกรณีนี้ฉันคิดว่าวิธีควรตรงกันเนื่องจากการโทรไปที่HttpClient.GetAsync(...)?
Benjamin Fox

คำตอบ:


468

คุณใช้ API ในทางที่ผิด

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

นี้จะจัดการโดย SynchronizationContextASP.NET

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

ดังนั้นนี่คือเหตุผลที่test5ล้มเหลว:

  • Test5Controller.GetดำเนินการAsyncAwait_GetSomeDataAsync(ภายในบริบทคำขอ ASP.NET)
  • AsyncAwait_GetSomeDataAsyncดำเนินการHttpClient.GetAsync(ภายในบริบทคำขอ ASP.NET)
  • การร้องขอ HTTP จะถูกส่งออกไปและส่งกลับยังไม่เสร็จสมบูรณ์HttpClient.GetAsyncTask
  • AsyncAwait_GetSomeDataAsyncรอคอยTask; เพราะมันยังไม่สมบูรณ์ส่งกลับยังไม่เสร็จสมบูรณ์AsyncAwait_GetSomeDataAsyncTask
  • Test5Controller.Get บล็อกเธรดปัจจุบันจนกว่าจะTaskเสร็จสมบูรณ์
  • การตอบสนอง HTTP เข้ามาและการTaskส่งคืนโดยHttpClient.GetAsyncจะเสร็จสมบูรณ์
  • AsyncAwait_GetSomeDataAsyncพยายามที่จะดำเนินการต่อภายในบริบทคำขอ ASP.NET แต่มีอยู่แล้วการตั้งกระทู้ในบริบทที่: Test5Controller.Getด้ายที่ถูกบล็อกใน
  • การหยุดชะงัก

นี่คือสาเหตุที่คนอื่นทำงาน:

  • ( test1, test2และtest3): Continuations_GetSomeDataAsyncจัดกำหนดการความต่อเนื่องของเธรดพูลนอกบริบทคำร้องขอ ASP.NET สิ่งนี้ทำให้การTaskส่งคืนโดยContinuations_GetSomeDataAsyncสมบูรณ์โดยไม่ต้องป้อนบริบทการร้องขออีกครั้ง
  • ( test4และtest6): เนื่องจากTaskกำลังรอเธรดคำร้องขอ ASP.NET จะไม่ถูกบล็อก สิ่งนี้อนุญาตให้AsyncAwait_GetSomeDataAsyncใช้บริบทคำร้องขอ ASP.NET เมื่อพร้อมที่จะดำเนินการต่อ

และนี่คือแนวทางปฏิบัติที่ดีที่สุด:

  1. ในasyncวิธีการ"ห้องสมุด" ของคุณให้ใช้ConfigureAwait(false)ทุกครั้งที่ทำได้ ในกรณีของคุณสิ่งนี้จะเปลี่ยนAsyncAwait_GetSomeDataAsyncเป็นvar result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
  2. อย่าปิดกั้นบนTasks; มันasyncลงไปหมดแล้ว กล่าวอีกนัยหนึ่งให้ใช้awaitแทนGetResult( Task.ResultและTask.Waitควรแทนที่ด้วยawait)

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

ข้อมูลมากกว่านี้:

ปรับปรุง 2012-07-13: Incorporated คำตอบนี้ลงในบล็อกโพสต์


2
มีเอกสารบางอย่างสำหรับ ASP.NET SynchroniztaionContextที่อธิบายว่าสามารถมีเธรดเดียวเท่านั้นในบริบทสำหรับคำขอบางอย่างได้หรือไม่ ถ้าไม่ฉันคิดว่าควรจะมี
svick

8
มันไม่ได้มีการบันทึกไว้ทุกที่ AFAIK
Stephen Cleary

10
ขอบคุณ - น่ากลัวการตอบสนอง ความแตกต่างของพฤติกรรมระหว่างรหัสที่เหมือนกันตามหน้าที่ (เห็นได้ชัด) นั้นน่าผิดหวัง แต่ก็สมเหตุสมผลกับคำอธิบายของคุณ มันจะมีประโยชน์ถ้าเฟรมเวิร์กสามารถตรวจจับการชะงักงันเช่นนั้นและเพิ่มข้อยกเว้นบางแห่ง
Benjamin Fox

3
มีสถานการณ์ที่ไม่แนะนำให้ใช้. ConfigureAwait (false) ในบริบท asp.net หรือไม่? ฉันคิดว่ามันควรจะใช้ได้เสมอและมันเป็นเพียงบริบท UI ที่ไม่ควรใช้เนื่องจากคุณต้องซิงค์กับ UI หรือว่าฉันพลาดประเด็นไป?
AlexGad

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

62

แก้ไข: โดยทั่วไปพยายามหลีกเลี่ยงการทำด้านล่างยกเว้นความพยายามครั้งสุดท้ายเพื่อหลีกเลี่ยงการหยุดชะงัก อ่านความคิดเห็นแรกจาก Stephen Cleary

การแก้ไขอย่างรวดเร็วจากที่นี่ แทนที่จะเขียน:

Task tsk = AsyncOperation();
tsk.Wait();

ลอง:

Task.Run(() => AsyncOperation()).Wait();

หรือถ้าคุณต้องการผลลัพธ์:

var result = Task.Run(() => AsyncOperation()).Result;

จากแหล่งที่มา (แก้ไขเพื่อให้ตรงกับตัวอย่างข้างต้น):

AsyncOperation จะถูกเรียกใช้บน ThreadPool โดยที่จะไม่มี SynchronizationContext และการดำเนินการต่อที่ใช้ภายใน AsyncOperation จะไม่ถูกบังคับให้กลับไปที่เธรดที่เรียกใช้

สำหรับฉันนี่ดูเหมือนเป็นตัวเลือกที่ใช้งานได้เนื่องจากฉันไม่มีตัวเลือกในการทำให้เป็นแบบซิงค์ตลอดทาง (ซึ่งฉันต้องการ)

จากแหล่งที่มา:

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

int Sync () {return Task.Run (() => Library.FooAsync ()) ผลลัพธ์; }

ตอนนี้ FooAsync จะถูกเรียกใช้บน ThreadPool ซึ่งจะไม่มี SynchronizationContext และการดำเนินการต่อที่ใช้ภายใน FooAsync จะไม่ถูกบังคับกลับไปยังเธรดที่เรียกใช้ Sync ()


7
อาจต้องการอ่านลิงค์ซอร์สของคุณอีกครั้ง ผู้เขียนไม่แนะนำให้ทำเช่นนี้ มันใช้งานได้หรือไม่ ใช่ แต่ในแง่ที่ว่าคุณหลีกเลี่ยงการหยุดชะงัก วิธีการแก้ปัญหานี้คัดค้านผลประโยชน์ทั้งหมดของasyncรหัสบน ASP.NET และในความเป็นจริงอาจทำให้เกิดปัญหาในระดับ BTW ConfigureAwaitไม่ "หยุดการทำงานของ async ที่เหมาะสม" ในทุกสถานการณ์ มันเป็นสิ่งที่คุณควรใช้ในรหัสห้องสมุด
Stephen Cleary

2
Avoid Exposing Synchronous Wrappers for Asynchronous Implementationsมันเป็นส่วนแรกทั้งชื่อตัวหนา ส่วนที่เหลือทั้งหมดของโพสต์ที่มีการอธิบายวิธีการที่แตกต่างกันไม่กี่ที่จะทำมันถ้าคุณอย่างต้องไป
Stephen Cleary

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

3
ฉันชอบคำตอบทั้งหมดที่นี่และเช่นเคย .... พวกเขาทั้งหมดขึ้นอยู่กับบริบท (ปุนตั้งใจฮ่า ๆ ) ฉันกำลังตัดการโทรแบบ Async ของ HttpClient ด้วยเวอร์ชันซิงโครนัสดังนั้นฉันไม่สามารถเปลี่ยนรหัสนั้นเพื่อเพิ่ม ConfigureAwait ในไลบรารีนั้นได้ ดังนั้นเพื่อป้องกันการหยุดชะงักในการผลิตฉันกำลังตัดการโทร Async ใน Task.Run ดังที่ฉันเข้าใจแล้วนี่จะใช้ 1 เธรดพิเศษต่อคำขอและหลีกเลี่ยงการหยุดชะงัก ฉันสันนิษฐานว่าเป็นไปตามอย่างสมบูรณ์ฉันต้องใช้วิธีการซิงค์ของ WebClient นั่นเป็นงานที่ต้องทำมากมายดังนั้นฉันจะต้องมีเหตุผลที่น่าสนใจไม่ยึดติดกับแนวทางปัจจุบันของฉัน
samneric

1
ฉันสิ้นสุดการสร้างวิธีการขยายเพื่อแปลง Async เป็น Sync ฉันอ่านที่นี่ที่ไหนสักแห่งในลักษณะเดียวกันกับ. Net framework มัน: public static TResult RunSync <TResult> (Func <Task <TResult>> func) {return _taskFactory .StartNew (func) .Unwrap () .GetAwaiter () .GetResult (); }
samneric

10

เนื่องจากคุณกำลังใช้.Resultหรือ.Waitหรือawaitนี้จะจบลงด้วยการที่ก่อให้เกิดการหยุดชะงักในรหัสของคุณ

คุณสามารถใช้วิธีการConfigureAwait(false)ในasyncการป้องกันการหยุดชะงัก

แบบนี้:

var result = await httpClient.GetAsync("http://stackoverflow.com", HttpCompletionOption.ResponseHeadersRead)
                             .ConfigureAwait(false);

คุณสามารถใช้ConfigureAwait(false)ทุกที่ที่เป็นไปได้สำหรับ Don't Block Async Code


2

โรงเรียนทั้งสองนี้ไม่ได้ยกเว้นจริงๆ

นี่คือสถานการณ์ที่คุณต้องใช้

   Task.Run(() => AsyncOperation()).Wait(); 

หรือสิ่งที่ชอบ

   AsyncContext.Run(AsyncOperation);

ฉันมีการกระทำ MVC ที่อยู่ภายใต้แอตทริบิวต์การทำธุรกรรมฐานข้อมูล ความคิดคือ (อาจ) เพื่อย้อนกลับทุกอย่างที่ทำในการกระทำหากมีอะไรผิดพลาด สิ่งนี้ไม่อนุญาตให้มีการสลับบริบทมิฉะนั้นการทำธุรกรรมย้อนกลับหรือการส่งมอบจะล้มเหลว

ไลบรารีที่ฉันต้องการคือ async ตามที่คาดว่าจะเรียกใช้ async

ตัวเลือกเท่านั้น เรียกใช้เป็นการโทรปกติ

ฉันแค่พูดกับแต่ละคน


ดังนั้นคุณแนะนำตัวเลือกแรกในคำตอบของคุณ?
Don Cheadle

1

ฉันจะใส่ลงไปในนี้เพื่อความสมบูรณ์มากกว่าความเกี่ยวข้องโดยตรงกับ OP ฉันใช้เวลาเกือบหนึ่งวันในการดีบักHttpClientคำขอโดยสงสัยว่าทำไมฉันไม่เคยได้รับคำตอบกลับมา

ในที่สุดก็พบว่าผมได้ลืมที่จะโทรไปลงสาย stackawaitasync

รู้สึกดีไม่แพ้อัฒภาค


-1

ฉันกำลังดูที่นี่:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter(v=vs.110).aspx

และที่นี่:

http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx

และเห็น:

ประเภทนี้และสมาชิกมีไว้สำหรับคอมไพเลอร์

เมื่อพิจารณาถึงawaitเวอร์ชันของงานและเป็นวิธีที่ 'ถูกต้อง' ในการทำสิ่งต่าง ๆ คุณต้องการคำตอบสำหรับคำถามนี้หรือไม่?

คะแนนของฉันคือวัตถุประสงค์ของ API


ฉันไม่ได้สังเกตว่าถึงแม้ว่าฉันจะได้เห็นภาษาอื่น ๆ ซึ่งบ่งชี้ว่าการใช้ GetResult () API เป็นกรณีการใช้งานที่รองรับ (และคาดว่า)
Benjamin Fox

1
ยิ่งไปกว่านั้นถ้าคุณ refactor Test5Controller.Get()เพื่อกำจัดพนักงานเสิร์ฟด้วยต่อไปนี้: var task = AsyncAwait_GetSomeDataAsync(); return task.Result;พฤติกรรมเดียวกันสามารถสังเกตได้
Benjamin Fox
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.