ความต่อเนื่องของงานบนเธรด UI


214

มีวิธี 'มาตรฐาน' เพื่อระบุว่างานต่อเนื่องควรทำงานบนเธรดที่งานเริ่มต้นถูกสร้างขึ้น?

ขณะนี้ฉันมีรหัสด้านล่าง - มันใช้งานได้ แต่คอยติดตามดิสแพตเชอร์และการสร้างแอคชั่นที่สองดูเหมือนจะเป็นค่าใช้จ่ายที่ไม่จำเป็น

dispatcher = Dispatcher.CurrentDispatcher;
Task task = Task.Factory.StartNew(() =>
{
    DoLongRunningWork();
});

Task UITask= task.ContinueWith(() =>
{
    dispatcher.Invoke(new Action(() =>
    {
        this.TextBlock1.Text = "Complete"; 
    }
});

ในกรณีตัวอย่างของคุณคุณสามารถใช้Control.Invoke(Action)เช่น TextBlock1.Invokeมากกว่าdispatcher.Invoke
พันเอก Panic

2
ขอบคุณ @ColonelPanic แต่ฉันใช้ WPF (ดังที่ติดแท็ก) ไม่ใช่ winforms
Greg Sansom

คำตอบ:


352

โทรติดต่อด้วยTaskScheduler.FromCurrentSynchronizationContext():

    Task UITask= task.ContinueWith(() =>
    {
     this.TextBlock1.Text = "Complete"; 
    }, TaskScheduler.FromCurrentSynchronizationContext());

สิ่งนี้เหมาะสมเฉพาะถ้าบริบทการดำเนินการปัจจุบันอยู่บนเธรด UI


39
ใช้ได้เฉพาะเมื่อบริบทการดำเนินการปัจจุบันอยู่บนเธรด UI หากคุณใส่รหัสนี้ในงานอื่นคุณจะได้รับ InvalidOperationException (ดูที่ส่วนข้อยกเว้น )
stukselbax

3
ใน. NET 4.5 ควรใช้คำตอบของ Johan Larsson เป็นวิธีมาตรฐานสำหรับความต่อเนื่องของงานในเธรด UI เพียงแค่เขียน: รอ Task.Run (DoLongRunningWork); this.TextBlock1.Text = "เสร็จสมบูรณ์"; ดูเพิ่มเติมได้ที่: blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
Marcel W

1
ขอบคุณที่ช่วยชีวิตฉัน ฉันใช้เวลาหลายชั่วโมงเพื่อหาวิธีเรียกเธรดหลักภายในรอ / ดำเนินการต่อด้วย สำหรับคนอื่นวิธีการใช้งานGoogle Firebase SDK สำหรับ Unityและยังคงมีปัญหาเดียวกันนี่เป็นวิธีการทำงาน
CHAP

2
@MarcelW - awaitเป็นรูปแบบที่ดี - แต่ถ้าคุณอยู่ในasyncบริบท (เช่นวิธีการประกาศasync) หากไม่เป็นเช่นนั้นก็ยังจำเป็นต้องทำบางสิ่งเช่นคำตอบนี้
ToolmakerSteve

33

ด้วย async คุณเพียงทำ:

await Task.Run(() => do some stuff);
// continue doing stuff on the same context as before.
// while it is the default it is nice to be explicit about it with:
await Task.Run(() => do some stuff).ConfigureAwait(true);

อย่างไรก็ตาม:

await Task.Run(() => do some stuff).ConfigureAwait(false);
// continue doing stuff on the same thread as the task finished on.

2
ความคิดเห็นภายใต้falseเวอร์ชันทำให้ฉันสับสน ผมคิดว่าfalseวิธีการที่มันอาจจะยังคงอยู่ในที่แตกต่างกันด้าย
ToolmakerSteve

1
@ToolmakerSteve ขึ้นอยู่กับหัวข้อที่คุณกำลังคิด เธรดผู้ปฏิบัติงานที่ใช้โดย Task.Run หรือเธรดผู้โทร? โปรดจำไว้ว่า "เธรดเดียวกันกับงานที่ทำเสร็จ" หมายถึงเธรดผู้ปฏิบัติงาน (หลีกเลี่ยง 'การสลับ' ระหว่างเธรด) นอกจากนี้ ConfigureAwait (จริง) ไม่รับประกันว่าการควบคุมจะส่งกลับไปยังเธรดเดียวกันเท่านั้นไปยังบริบทเดียวกัน(แม้ว่าความแตกต่างอาจไม่สำคัญ)
Max Barraclough

@ MaxBarraclough - ขอบคุณฉันอ่านผิดว่า "thread เดียวกัน" มีความหมายว่าอย่างไร หลีกเลี่ยงการสลับระหว่างเธรดในแง่ของการเพิ่มประสิทธิภาพโดยการใช้เธรดใดก็ตามที่เกิดขึ้นเพื่อเรียกใช้ [เพื่อทำงาน "ทำบางสิ่ง" ที่จะทำให้มันชัดเจนสำหรับฉัน
ToolmakerSteve

1
คำถามไม่ได้ระบุว่าอยู่ภายในasyncเมธอด (ซึ่งจำเป็นต้องใช้await) คำตอบเมื่อawaitไม่สามารถใช้ได้คืออะไร?
ToolmakerSteve

22

หากคุณมีค่าส่งคืนคุณต้องส่งไปยัง UI คุณสามารถใช้เวอร์ชันทั่วไปดังนี้:

สิ่งนี้กำลังถูกเรียกจาก MVVM ViewModel ในกรณีของฉัน

var updateManifest = Task<ShippingManifest>.Run(() =>
    {
        Thread.Sleep(5000);  // prove it's really working!

        // GenerateManifest calls service and returns 'ShippingManifest' object 
        return GenerateManifest();  
    })

    .ContinueWith(manifest =>
    {
        // MVVM property
        this.ShippingManifest = manifest.Result;

        // or if you are not using MVVM...
        // txtShippingManifest.Text = manifest.Result.ToString();    

        System.Diagnostics.Debug.WriteLine("UI manifest updated - " + DateTime.Now);

    }, TaskScheduler.FromCurrentSynchronizationContext());

ฉันคาดเดา = ก่อน GenerateManifest เป็นตัวพิมพ์ผิด
Sebastien F.

ใช่ - หายไปแล้ว! ขอบคุณ.
Simon_Weaver

11

ฉันแค่อยากจะเพิ่มเวอร์ชั่นนี้เพราะมันเป็นเธรดที่มีประโยชน์และฉันคิดว่านี่เป็นการใช้งานที่ง่ายมาก ฉันใช้หลายครั้งในรูปแบบต่าง ๆ หากแอปพลิเคชันแบบมัลติเธรด:

 Task.Factory.StartNew(() =>
      {
        DoLongRunningWork();
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() =>
              { txt.Text = "Complete"; }));
      });

2
ไม่ downvoting เนื่องจากนี่เป็นโซลูชันที่ทำงานได้ในบางสถานการณ์ อย่างไรก็ตามคำตอบที่ยอมรับนั้นดีกว่า เป็นเทคโนโลยีที่ไม่เชื่อเรื่องพระเจ้า ( TaskSchedulerเป็นส่วนหนึ่งของ BCL Dispatcherไม่ใช่) และสามารถใช้ในการเขียนกลุ่มงานที่ซับซ้อนเนื่องจากไม่ต้องกังวลเกี่ยวกับการดำเนินการ async แบบไฟและลืม (เช่นBeginInvoke)
Kirill Shlenskiy

@Killill คุณสามารถขยายได้เล็กน้อยเนื่องจากเธรด SO บางตัวได้ประกาศผู้มอบหมายงานเป็นวิธีที่ถูกต้องหากใช้ WPF ของ WinForms: หนึ่งสามารถเรียกใช้การอัปเดต GUI ได้ทั้งแบบอะซิงโครนัส (ใช้ BeginInvoke) หรือแบบซิงโครนัส ใช้เพราะจะไม่ต้องการบล็อกเธรดพื้นหลังเพียงเพื่อการปรับปรุง GUI FromCurrentSynchronizationContext ไม่ได้วางภารกิจการต่อเนื่องลงในคิวข้อความเธรดหลักด้วยวิธีเดียวกับโปรแกรมเลือกจ่ายงานหรือไม่
คณบดี

1
ใช่ แต่ OP กำลังถามเกี่ยวกับ WPF (และติดแท็กมันอย่างนั้น) และไม่ต้องการให้มีการอ้างอิงถึงโปรแกรมเลือกจ่ายงานใด ๆ (และฉันถือว่าการซิงโครไนซ์บริบทใด ๆ - คุณสามารถรับสิ่งนี้ได้จากเธรดหลัก เก็บการอ้างอิงไว้ที่ใดที่หนึ่ง) นี่คือเหตุผลที่ฉันชอบวิธีแก้ปัญหาที่ฉันโพสต์: มีการอ้างอิงแบบสแตติกเซฟ - เธรดในตัวที่ไม่ต้องการสิ่งนี้ ฉันคิดว่านี่เป็นประโยชน์อย่างมากในบริบท WPF
คณบดี

3
เพียงแค่ต้องการเสริมความคิดเห็นล่าสุดของฉัน: นักพัฒนาไม่เพียง แต่จะต้องเก็บบริบทการซิงค์ แต่เขา / เธอต้องรู้ว่าสิ่งนี้สามารถใช้ได้จากเธรดหลักเท่านั้น ปัญหานี้เป็นสาเหตุของความสับสนในคำถาม SO หลายสิบคำถาม: ผู้คนตลอดเวลาพยายามรับสิ่งนั้นจากเธรดผู้ปฏิบัติงาน หากรหัสของพวกเขาถูกย้ายไปที่เธรดผู้ปฏิบัติงานมันจะล้มเหลวเนื่องจากปัญหานี้ ดังนั้นเนื่องจากความชุกของ WPF สิ่งนี้จึงควรถูกอธิบายอย่างชัดเจนในคำถามยอดนิยมนี้
คณบดี

1
... อย่างไรก็ตามการสังเกตของ Dean เกี่ยวกับ [คำตอบที่ยอมรับ] จำเป็นต้องติดตามบริบทการซิงค์หากโค้ดอาจไม่อยู่ในเธรดหลักเป็นสิ่งสำคัญที่ควรทราบและการหลีกเลี่ยงที่เป็นประโยชน์ของคำตอบนี้
ToolmakerSteve

1

มาที่นี่ผ่าน google เพราะฉันกำลังมองหาวิธีที่ดีในการทำสิ่งต่าง ๆ บนเธรด ui หลังจากอยู่ใน Task.Run call - ใช้รหัสต่อไปนี้คุณสามารถใช้awaitเพื่อกลับไปที่ UI Thread ได้อีกครั้ง

ฉันหวังว่านี่จะช่วยให้ใครบางคน

public static class UI
{
    public static DispatcherAwaiter Thread => new DispatcherAwaiter();
}

public struct DispatcherAwaiter : INotifyCompletion
{
    public bool IsCompleted => Application.Current.Dispatcher.CheckAccess();

    public void OnCompleted(Action continuation) => Application.Current.Dispatcher.Invoke(continuation);

    public void GetResult() { }

    public DispatcherAwaiter GetAwaiter()
    {
        return this;
    }
}

การใช้งาน:

... code which is executed on the background thread...
await UI.Thread;
... code which will be run in the application dispatcher (ui thread) ...

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