เมื่อใช้ Task.Run อย่างถูกต้องและเมื่อรอเพียง async


317

Task.Runผมอยากจะขอให้คุณอยู่กับความเห็นของคุณเกี่ยวกับสถาปัตยกรรมที่ถูกต้องเมื่อใช้ ฉันกำลังประสบกับความล้าหลัง UI ในแอปพลิเคชัน WPF .NET 4.5 ของเรา (ด้วยกรอบ Caliburn Micro)

โดยทั่วไปฉันกำลังทำ (ตัวอย่างโค้ดที่ง่ายมาก):

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // Makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync();

      HideLoadingAnimation();
   }
}

public class ContentLoader
{
    public async Task LoadContentAsync()
    {
        await DoCpuBoundWorkAsync();
        await DoIoBoundWorkAsync();
        await DoCpuBoundWorkAsync();

        // I am not really sure what all I can consider as CPU bound as slowing down the UI
        await DoSomeOtherWorkAsync();
    }
}

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

ที่ที่ดีที่สุดในการวาง Task.Run อยู่ที่ไหน

ฉันควรจะแค่

  1. ตัดการเรียกภายนอกเนื่องจากการทำงานของเธรดน้อยกว่าสำหรับ. NET

  2. หรือฉันควรจะห่อเฉพาะวิธีที่ผูกกับ CPU ที่ทำงานอยู่ภายในTask.Runเพราะจะทำให้มันสามารถนำกลับมาใช้ใหม่ได้สำหรับที่อื่น ๆ ? ฉันไม่แน่ใจว่าที่นี่หากเริ่มทำงานกับเธรดพื้นหลังที่อยู่ในแกนลึกเป็นความคิดที่ดี

โฆษณา (1) ทางออกแรกจะเป็นเช่นนี้:

public async void Handle(SomeMessage message)
{
    ShowLoadingAnimation();
    await Task.Run(async () => await this.contentLoader.LoadContentAsync());
    HideLoadingAnimation();
}

// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.

โฆษณา (2) ทางออกที่สองจะเป็นเช่นนี้:

public async Task DoCpuBoundWorkAsync()
{
    await Task.Run(() => {
        // Do lot of work here
    });
}

public async Task DoSomeOtherWorkAsync(
{
    // I am not sure how to handle this methods -
    // probably need to test one by one, if it is slowing down UI
}

BTW สายใน (1) ก็ควรจะawait Task.Run(async () => await this.contentLoader.LoadContentAsync()); await Task.Run( () => this.contentLoader.LoadContentAsync() );AFAIK คุณไม่ได้อะไรโดยการเพิ่มเป็นครั้งที่สองawaitและภายในasync และเนื่องจากคุณไม่ได้ผ่านพารามิเตอร์ช่วยลดความยุ่งยากมากขึ้นเล็กน้อยมาอยู่ที่Task.Run await Task.Run( this.contentLoader.LoadContentAsync );
ToolmakerSteve

จริงๆแล้วมันมีความแตกต่างเล็กน้อยถ้าคุณรอคอยอยู่ข้างใน ดูนี้บทความ ฉันพบว่ามันมีประโยชน์มากเพียงแค่ในจุดนี้ฉันไม่เห็นด้วยและชอบกลับไปที่ Task โดยตรงแทนที่จะรอ (ตามที่คุณแนะนำในความคิดเห็นของคุณ)
Lukas K

คำตอบ:


365

สังเกตแนวทางในการทำงานกับเธรด UI ที่รวบรวมในบล็อกของฉัน:

  • อย่าปิดกั้นเธรด UI นานกว่า 50 มิลลิวินาทีในแต่ละครั้ง
  • คุณสามารถกำหนดเวลา ~ 100 ต่อเนื่องบนเธรด UI ต่อวินาที 1,000 มากเกินไป

มีสองเทคนิคที่คุณควรใช้:

1) ใช้ConfigureAwait(false)เมื่อคุณสามารถ

เช่นแทนawait MyAsync().ConfigureAwait(false);await MyAsync();

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

สำหรับข้อมูลเพิ่มเติมโปรดดูบทความ MSDN ของฉันปฏิบัติที่ดีที่สุดในการเขียนโปรแกรมไม่ตรงกัน

2) ใช้Task.Runเพื่อเรียกใช้เมธอดที่ผูกกับ CPU

คุณควรใช้Task.Runแต่ไม่อยู่ในรหัสใด ๆ ที่คุณต้องการให้นำมาใช้ซ้ำ (เช่นรหัสห้องสมุด) ดังนั้นคุณใช้Task.Runเพื่อเรียกวิธีการที่ไม่ได้เป็นส่วนหนึ่งของการดำเนินงานของวิธีการ

ดังนั้นการทำงานกับ CPU ล้วนๆจะมีลักษณะเช่นนี้:

// Documentation: This method is CPU-bound.
void DoWork();

สิ่งที่คุณจะโทรโดยใช้Task.Run:

await Task.Run(() => DoWork());

วิธีการที่มีส่วนผสมของ CPU-bound และ I / O-bound ควรมีAsyncลายเซ็นต์พร้อมเอกสารประกอบที่ชี้ให้เห็นลักษณะของ CPU-bound:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

ซึ่งคุณจะต้องโทรหาโดยใช้Task.Run(เนื่องจากเป็นขอบของ CPU บางส่วน):

await Task.Run(() => DoWorkAsync());

4
ขอบคุณสำหรับการตอบสนองที่รวดเร็วของคุณ! ฉันรู้ลิงค์ที่คุณโพสต์และดูวิดีโอที่อ้างอิงในบล็อกของคุณ จริงๆแล้วนั่นคือเหตุผลที่ฉันโพสต์คำถามนี้ - ในวิดีโอมีการกล่าวถึง (เช่นเดียวกับในการตอบสนองของคุณ) คุณไม่ควรใช้ Task.Run ในรหัสหลัก แต่ปัญหาของฉันคือฉันต้องห่อวิธีดังกล่าวทุกครั้งที่ฉันใช้เพื่อไม่ทำให้การตอบสนองช้าลง (โปรดทราบว่ารหัสทั้งหมดของฉันเป็นแบบอะซิงก์และไม่ได้บล็อก แต่ไม่มี Thread.Run ฉันสับสนเช่นกันว่าเป็นวิธีที่ดีกว่าหรือไม่ในการห่อวิธีการเชื่อมโยง CPU (การเรียก Task.Run หลายสาย) หรือการรวมทุกอย่างไว้ใน Task.Run อย่างสมบูรณ์?
Lukas K

12
ConfigureAwait(false)ทุกวิธีห้องสมุดของคุณควรจะใช้ หากคุณทำสิ่งนั้นก่อนคุณอาจพบว่าTask.Runไม่จำเป็นอย่างสมบูรณ์ หากคุณไม่ยังคงต้องTask.Runแล้วมันไม่ได้สร้างความแตกต่างมากในการรันไทม์ในกรณีนี้ไม่ว่าคุณจะเรียกมันว่าครั้งเดียวหรือหลายครั้งดังนั้นเพียงแค่ทำในสิ่งที่เป็นธรรมชาติมากที่สุดสำหรับรหัสของคุณ
Stephen Cleary

2
ฉันไม่เข้าใจว่าเทคนิคแรกจะช่วยเขาได้อย่างไร แม้ว่าคุณจะใช้ConfigureAwait(false)กับวิธี cpu-bound ของคุณ แต่ก็ยังคงเป็นเธรด UI ที่จะทำตามวิธี cpu-bound และทุกอย่างหลังจากเท่านั้นที่สามารถทำได้บนเธรด TP หรือฉันเข้าใจผิดบางอย่าง?
Darius

4
@ user4205580: ไม่Task.Runเข้าใจลายเซ็นแบบอะซิงโครนัสดังนั้นจึงไม่สมบูรณ์จนกว่าDoWorkAsyncจะเสร็จสมบูรณ์ ส่วนเสริมasync/ awaitไม่จำเป็น ฉันอธิบายเพิ่มเติมของ "ทำไม" ในของฉันชุดบล็อกเกี่ยวกับTask.Runมารยาท
Stephen Cleary

3
@ user4205580: ไม่วิธีการ async "core" ส่วนใหญ่ไม่ได้ใช้เป็นการภายใน ปกติวิธีการใช้ "หลัก" วิธีการ async คือการใช้หรือหนึ่งในสัญลักษณ์จดชวเลขเช่นTaskCompletionSource<T> FromAsyncฉันมีบล็อกโพสต์ที่จะไปลงในรายละเอียดมากขึ้นว่าทำไมวิธี async ไม่จำเป็นต้องมีหัวข้อ
Stephen Cleary

12

ปัญหาหนึ่งของ ContentLoader ของคุณก็คือภายในจะทำงานตามลำดับ รูปแบบที่ดีกว่าคือการทำให้งานเป็นเส้นขนานและจากนั้นทำการเรียงลำดับตอนท้ายเพื่อให้ได้

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync(); 

      HideLoadingAnimation();   
   }
}

public class ContentLoader 
{
    public async Task LoadContentAsync()
    {
        var tasks = new List<Task>();
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoIoBoundWorkAsync());
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoSomeOtherWorkAsync());

        await Task.WhenAll(tasks).ConfigureAwait(false);
    }
}

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

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