วิธีปฏิบัติที่ดีที่สุดในการโทร ConfigureAwait สำหรับรหัสฝั่งเซิร์ฟเวอร์ทั้งหมด


561

เมื่อคุณมีรหัสฝั่งเซิร์ฟเวอร์ (เช่นบางส่วนApiController) และฟังก์ชั่นของคุณจะไม่ตรงกัน - เพื่อให้พวกเขากลับมาTask<SomeObject>- มันถือว่าเป็นวิธีที่ดีที่สุดว่าเวลาที่คุณรอคอยฟังก์ชั่นที่คุณโทรหาConfigureAwait(false)?

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

ฉันได้พิมพ์ตัวอย่างของสิ่งที่ฉันพูดถึงด้านล่าง:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}

คำตอบ:


628

ปรับปรุง: ASP.NET SynchronizationContextหลักไม่ได้ ถ้าคุณอยู่บน ASP.NET Core มันไม่สำคัญว่าคุณจะใช้ConfigureAwait(false)หรือไม่

สำหรับ ASP.NET "Full" หรือ "Classic" หรืออะไรก็ตามคำตอบที่เหลือก็ยังคงมีผล

โพสต์ต้นฉบับ (สำหรับ ASP.NET ที่ไม่ใช่คอร์):

วิดีโอนี้โดยทีม ASP.NET มีข้อมูลที่ดีที่สุดเกี่ยวกับการใช้asyncบน ASP.NET

ฉันได้อ่านแล้วว่ามันมีประสิทธิภาพมากกว่าเพราะไม่ต้องสลับบริบทของเธรดกลับไปเป็นบริบทของเธรดดั้งเดิม

สิ่งนี้เป็นจริงกับแอปพลิเคชัน UI ซึ่งมีเพียงเธรด UI เดียวที่คุณต้อง "ซิงค์" กลับไป

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

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

อย่างไรก็ตามด้วย ASP.NET Web Api หากคำขอของคุณเข้ามาในเธรดเดียวและคุณรอฟังก์ชั่นและเรียกใช้ ConfigureAwait (false) ที่อาจทำให้คุณอยู่ในเธรดอื่นเมื่อคุณส่งคืนผลลัพธ์สุดท้ายของฟังก์ชัน ApiController ของคุณ .

ที่จริงแล้วการทำสิ่งawaitนั้นทำได้ เมื่อคุณasyncวิธีฮิตawaitที่วิธีการที่ถูกปิดกั้น แต่ด้ายกลับไปที่สระว่ายน้ำด้าย เมื่อเมธอดพร้อมที่จะดำเนินการต่อเธรดใด ๆ จะถูกกระชากจากเธรดพูลและใช้เพื่อดำเนินการเมธอดต่อ

ความแตกต่างเพียงอย่างเดียวที่ConfigureAwaitเกิดขึ้นใน ASP.NET คือเธรดนั้นเข้าสู่บริบทคำขอเมื่อดำเนินการเมธอดต่อ

ผมมีข้อมูลพื้นฐานอื่น ๆ ในของฉันบทความ MSDN บนSynchronizationContextของฉันและโพสต์บล็อกบทนำasync


23
หน่วยเก็บข้อมูลเธรดโลคัลไม่ได้ถูกส่งผ่านบริบทใด ๆ HttpContext.Currentจะไหลโดย ASP.NET SynchronizationContextซึ่งจะไหลออกมาโดยค่าเริ่มต้นเมื่อคุณแต่มันไม่ได้ไหลออกมาโดยawait ContinueWithOTOH บริบทการดำเนินการ (รวมถึงข้อ จำกัด ด้านความปลอดภัย) เป็นบริบทที่กล่าวถึงใน CLR ผ่านทาง C # และมีการไหลโดยทั้งสองContinueWithและawait(แม้ว่าคุณจะใช้ConfigureAwait(false))
สตีเฟ่นเคลียร์

65
จะดีหรือไม่ถ้าภาษา C # รองรับภาษา ConfigureAwait (false)? บางอย่างเช่น 'awaitnc' (ไม่ต้องรอบริบท) การพิมพ์การเรียกเมธอดแยกต่างหากทุกที่นั้นค่อนข้างน่ารำคาญ :)
NathanAldenSr

19
@NathanAldenSr: มีการพูดคุยกันค่อนข้างน้อย ปัญหาเกี่ยวกับคำหลักใหม่นั้นConfigureAwaitจริง ๆ แล้วจะสมเหตุสมผลเมื่อคุณรองานในขณะที่awaitทำหน้าที่ "รอ" ตัวเลือกอื่น ๆ ที่พิจารณาคือ: พฤติกรรมเริ่มต้นควรละทิ้งบริบทหากอยู่ในไลบรารีหรือไม่ หรือมีการตั้งค่าคอมไพเลอร์สำหรับพฤติกรรมบริบทเริ่มต้น? ทั้งสองอย่างนี้ถูกปฏิเสธเพราะมันยากที่จะอ่านโค้ดและบอกว่ามันทำอะไรได้ยาก
Stephen Cleary

10
@AnshulNigam: ซึ่งเป็นสาเหตุที่การดำเนินการควบคุมต้องการบริบทของพวกเขา แต่วิธีการส่วนใหญ่ที่เรียกการกระทำไม่ได้
Stephen Cleary

14
@JonathanRoeder: โดยทั่วไปคุณไม่จำเป็นต้องConfigureAwait(false)หลีกเลี่ยงการหยุดชะงักResult/ Wait- ตามเพราะใน ASP.NET คุณไม่ควรใช้Result/ Waitในตอนแรก
Stephen Cleary

131

คำตอบสั้น ๆ สำหรับคำถามของคุณ: ไม่คุณไม่ควรโทรหาConfigureAwait(false)แอพพลิเคชั่นในระดับนั้น

รุ่น DR ของคำตอบที่ยาว TL: ถ้าคุณเขียนห้องสมุดที่คุณไม่ทราบว่าผู้บริโภคของคุณและไม่จำเป็นต้องประสานบริบท (ซึ่งคุณไม่ควรในห้องสมุดผมเชื่อว่า) ConfigureAwait(false)คุณควรใช้ มิฉะนั้นผู้บริโภคในห้องสมุดของคุณอาจเผชิญกับการหยุดชะงักโดยใช้วิธีอะซิงโครนัสของคุณในรูปแบบการบล็อก ขึ้นอยู่กับสถานการณ์

นี่คือคำอธิบายรายละเอียดเล็กน้อยเกี่ยวกับความสำคัญของConfigureAwaitวิธีการ (อ้างจากโพสต์บล็อกของฉัน):

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

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

นอกจากนี้ยังมีบทความที่ดีสองบทความสำหรับคุณซึ่งตรงกับคำถามของคุณ:

ในที่สุดก็มีวิดีโอสั้น ๆ ที่ดีจากลูเชีย Wischikตรงกับหัวข้อนี้: Async วิธีห้องสมุดควรพิจารณาการใช้ Task.ConfigureAwait (เท็จ)

หวังว่านี่จะช่วยได้


2
"เมธอด GetAwaiter ของงานค้นหา SynchronizationContext.Current หากบริบทการซิงโครไนซ์ปัจจุบันไม่เป็นโมฆะ - ฉันได้รับความประทับใจว่าคุณกำลังพยายามพูดว่าTaskเดินกองเพื่อรับSynchronizationContextซึ่งผิด SynchronizationContextจะคว้าก่อนที่จะเรียกไปTaskแล้วส่วนที่เหลือของรหัสเป็นอย่างต่อเนื่องบนSynchronizationContextถ้าSynchronizationContext.Currentเป็นไม่เป็นโมฆะ
casperOne

1
@casperOne ฉันตั้งใจจะพูดแบบเดียวกัน
tugberk

8
ไม่ควรเป็นความรับผิดชอบของผู้โทรที่จะตรวจสอบให้แน่ใจว่าSynchronizationContext.Currentมีความชัดเจน / หรือมีการเรียกใช้ไลบรารีภายในTask.Run()แทนที่จะต้องเขียน.ConfigureAwait(false)ทั่วไลบรารีคลาส?
binki

1
@binki - ในทางกลับกัน: (1) สมมุติว่ามีการใช้ไลบรารีในหลาย ๆ แอปพลิเคชั่นดังนั้นการพยายามใช้ไลบรารีครั้งเดียวเพื่อทำให้แอปพลิเคชันง่ายขึ้นประหยัดค่าใช้จ่าย (2) ผู้เขียนสันนิษฐานห้องสมุดรู้ว่าเขาได้เขียนรหัสที่มีเหตุผลที่จะต้องดำเนินการต่อไปในบริบทเดิมซึ่งเขาแสดงออกโดยผู้ที่ไม่มี.ConfigureAwait(false)s บางทีมันอาจจะง่ายขึ้นสำหรับผู้เขียนห้องสมุดหากนั่นเป็นพฤติกรรมเริ่มต้น แต่ฉันคิดว่าการเขียนไลบรารีอย่างถูกต้องยากขึ้นเล็กน้อยจะดีกว่าการเขียนแอปอย่างถูกต้อง
ToolmakerSteve

4
เหตุใดผู้เขียนไลบรารีจึงต้องให้รหัสแก่ผู้บริโภค หากผู้บริโภคต้องการหยุดชะงักทำไมฉันต้องป้องกันพวกเขา
Quarkly

25

ข้อเสียเปรียบที่ยิ่งใหญ่ที่สุดที่ฉันเคยพบในการใช้ ConfigureAwait (false) ก็คือวัฒนธรรมของเธรดจะถูกเปลี่ยนกลับไปเป็นค่าเริ่มต้นของระบบ หากคุณกำหนดค่าวัฒนธรรมเช่น ...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

และคุณกำลังโฮสต์บนเซิร์ฟเวอร์ที่มีการตั้งค่าวัฒนธรรมเป็น en-US จากนั้นคุณจะพบก่อนที่ ConfigureAwait (false) เรียกว่า CultureInfo.CurrentCulture จะส่งคืน en-AU และหลังจากคุณได้รับ en-US กล่าวคือ

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

หากแอปพลิเคชันของคุณกำลังทำอะไรที่ต้องใช้การจัดรูปแบบข้อมูลเฉพาะวัฒนธรรมคุณจะต้องคำนึงถึงเรื่องนี้เมื่อใช้ ConfigureAwait (false)


27
. NET สมัยใหม่รุ่น (ฉันคิดว่าตั้งแต่ 4.6?) จะเผยแพร่วัฒนธรรมข้ามกระทู้แม้ว่าConfigureAwait(false)จะใช้
สตีเฟ่นเคลียร์

1
ขอบคุณสำหรับข้อมูล. เราใช้. net 4.5.2
Mick

11

ฉันมีความคิดทั่วไปเกี่ยวกับการดำเนินการของTask:

  1. เป็นงานที่ใช้แล้วทิ้ง แต่เราจะไม่ควรที่จะusingใช้
  2. ConfigureAwaitเป็นที่รู้จักใน 4.5 Taskถูกนำมาใช้ใน 4.0
  3. . NET Threads มักจะใช้ในการไหลบริบท (ดู C # ผ่านทางหนังสือ CLR) แต่ในการใช้งานเริ่มต้นของTask.ContinueWithพวกเขาไม่ได้ b / c มันตระหนักถึงการสลับบริบทมีราคาแพงและมันถูกปิดโดยค่าเริ่มต้น
  4. ปัญหาคือผู้พัฒนาห้องสมุดไม่ควรสนใจว่าลูกค้าต้องการโฟลว์บริบทหรือไม่ดังนั้นจึงไม่ควรตัดสินใจว่าจะโฟลว์บริบทหรือไม่
  5. [เพิ่มในภายหลัง] ความจริงที่ว่าไม่มีคำตอบที่เชื่อถือได้และมีการอ้างอิงที่เหมาะสมและเรายังคงต่อสู้กับสิ่งนี้หมายความว่าบางคนทำงานไม่ถูกต้อง

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

ปัญหาคือเมื่อไลบรารี exposes API แบบซิงโครนัส แต่ใช้ asynchronous API อื่น - ดังนั้นคุณต้องใช้Wait()/ Resultในรหัสของคุณ


6
1) คุณสามารถโทรTask.Disposeถ้าคุณต้องการ; คุณไม่จำเป็นต้องใช้เวลาส่วนใหญ่ 2) Taskเป็นที่รู้จักใน .NET 4.0 เป็นส่วนหนึ่งของ TPL ซึ่งไม่จำเป็นต้องConfigureAwait; เมื่อasyncถูกเพิ่มเข้ามาพวกเขากลับมาใช้ใหม่ที่มีอยู่ชนิดแทนการประดิษฐ์ใหม่Task Future
Stephen Cleary

6
3) คุณสับสน "บริบท" สองประเภท "บริบท" ที่กล่าวถึงใน C # ผ่าน CLR นั้นจะมีการไหลเวียนเสมอแม้ในTasks; ว่า "บริบท" ควบคุมโดยContinueWithเป็นหรือSynchronizationContext TaskSchedulerบริบทที่แตกต่างเหล่านี้มีการอธิบายในรายละเอียดในบล็อกของสตีเฟ่น Toub ของ
สตีเฟ่นเคลียร์

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

1
ในตอนแรกคุณดูเหมือนจะบ่นแทนที่จะตอบคำถาม แล้วคุณกำลังพูดถึง "บริบท" ยกเว้นมีบริบทหลายประเภทใน. Net และจริงๆแล้วมันไม่ชัดเจนว่าคุณกำลังพูดถึงเรื่องใด และแม้ว่าคุณจะไม่สับสนตัวเอง (แต่ฉันคิดว่าคุณเป็นฉันเชื่อว่าไม่มีบริบทที่เคยไหลกับThreads แต่ไม่ได้อีกต่อไปด้วยContinueWith()) สิ่งนี้ทำให้คำตอบของคุณสับสนในการอ่าน
svick

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