ฉันจะอัพเดต GUI จากเธรดอื่นได้อย่างไร?


1392

ซึ่งเป็นวิธีที่ง่ายที่สุดในการปรับปรุงLabelจากที่อื่นThread?

  • ฉันมีการFormทำงานthread1และจากที่ฉันเริ่มหัวข้ออื่น ( thread2)

  • ในขณะที่thread2กำลังประมวลผลไฟล์บางไฟล์ฉันต้องการที่จะอัปเดตLabelในFormกับสถานะปัจจุบันของการthread2ทำงานของ

ฉันจะทำสิ่งนั้นได้อย่างไร


25
ไม่. net 2.0+ มีคลาส BackgroundWorker สำหรับสิ่งนี้เท่านั้น เป็นเธรด UI ที่ทราบ 1. สร้าง BackgroundWorker 2. เพิ่มผู้รับมอบสิทธิ์สองคน (หนึ่งคนสำหรับการดำเนินการและอีกหนึ่งคนเพื่อสำเร็จ)
Preet Sangha

13
อาจจะสายไปสักหน่อย: codeproject.com/KB/cs/Threadsafe_formupdating.aspx
MichaelD

4
ดูคำตอบสำหรับ. NET 4.5 และ C # 5.0: stackoverflow.com/a/18033198/2042090
Ryszard Dżegan

5
คำถามนี้ใช้ไม่ได้กับ Gtk # GUI สำหรับ Gtk # เห็นนี้และนี้คำตอบ
hlovdal

ระวัง: คำตอบสำหรับคำถามนี้เป็นระเบียบของ OT ("นี่คือสิ่งที่ฉันทำสำหรับแอพ WPF ของฉัน") และสิ่งประดิษฐ์. NET 2.0 ที่ผ่านมา
Marc L.

คำตอบ:


768

สำหรับ. NET 2.0 นี่เป็นโค้ดที่ดีที่ฉันเขียนซึ่งทำในสิ่งที่คุณต้องการอย่างแน่นอนและใช้ได้กับคุณสมบัติใด ๆ ในControl:

private delegate void SetControlPropertyThreadSafeDelegate(
    Control control, 
    string propertyName, 
    object propertyValue);

public static void SetControlPropertyThreadSafe(
    Control control, 
    string propertyName, 
    object propertyValue)
{
  if (control.InvokeRequired)
  {
    control.Invoke(new SetControlPropertyThreadSafeDelegate               
    (SetControlPropertyThreadSafe), 
    new object[] { control, propertyName, propertyValue });
  }
  else
  {
    control.GetType().InvokeMember(
        propertyName, 
        BindingFlags.SetProperty, 
        null, 
        control, 
        new object[] { propertyValue });
  }
}

เรียกว่าเป็นแบบนี้:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

หากคุณใช้. NET 3.0 ขึ้นไปคุณสามารถเขียนวิธีการด้านบนเป็นวิธีการขยายของControlคลาสซึ่งจะทำให้การโทรไปที่:

myLabel.SetPropertyThreadSafe("Text", status);

อัพเดท 05/10/2010:

สำหรับ. NET 3.0 คุณควรใช้รหัสนี้:

private delegate void SetPropertyThreadSafeDelegate<TResult>(
    Control @this, 
    Expression<Func<TResult>> property, 
    TResult value);

public static void SetPropertyThreadSafe<TResult>(
    this Control @this, 
    Expression<Func<TResult>> property, 
    TResult value)
{
  var propertyInfo = (property.Body as MemberExpression).Member 
      as PropertyInfo;

  if (propertyInfo == null ||
      !@this.GetType().IsSubclassOf(propertyInfo.ReflectedType) ||
      @this.GetType().GetProperty(
          propertyInfo.Name, 
          propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (@this.InvokeRequired)
  {
      @this.Invoke(new SetPropertyThreadSafeDelegate<TResult> 
      (SetPropertyThreadSafe), 
      new object[] { @this, property, value });
  }
  else
  {
      @this.GetType().InvokeMember(
          propertyInfo.Name, 
          BindingFlags.SetProperty, 
          null, 
          @this, 
          new object[] { value });
  }
}

ซึ่งใช้ LINQ และแลมบ์ดานิพจน์เพื่อให้ได้ไวยากรณ์ที่สะอาดขึ้นง่ายขึ้นและปลอดภัยยิ่งขึ้น:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

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

น่าเสียดายที่สิ่งนี้ไม่ได้ขัดขวางไม่ให้ใครทำสิ่งที่โง่เขลาเช่นการส่งผ่านControlทรัพย์สินและคุณค่าของผู้อื่นดังนั้นสิ่งต่อไปนี้จะรวบรวมอย่างมีความสุข:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

ดังนั้นฉันจึงเพิ่มการตรวจสอบรันไทม์เพื่อให้แน่ใจว่าคุณสมบัติที่ผ่านเข้ามาไม่ได้เป็นของControlวิธีการที่เรียกใช้ ไม่สมบูรณ์ แต่ยังดีกว่ารุ่น. NET 2.0 มาก

หากใครมีข้อเสนอแนะเพิ่มเติมเกี่ยวกับวิธีการปรับปรุงรหัสนี้เพื่อความปลอดภัยเวลารวบรวมโปรดแสดงความคิดเห็น!


3
มีหลายกรณีที่ this.GetType () ประเมินผลเหมือนกับ propertyInfo.ReflectedType (เช่น LinkLabel บน WinForms) ฉันไม่มีประสบการณ์ C # ขนาดใหญ่ แต่ฉันคิดว่าเงื่อนไขการยกเว้นควรเป็น: if (propertyInfo == null || (!@this.GetType (). IsSubclassOf (propertyInfo.ReflectedType) && @ this.GetType ( )! = propertyInfo.ReflectedType) || @ this.GetType (). GetProperty (propertyInfo.Name, propertyInfo.PropertyType) == null)
Corvin

9
@lan สามารถSetControlPropertyThreadSafe(myLabel, "Text", status)เรียกได้จากโมดูลหรือคลาสหรือแบบฟอร์มอื่น
Smith

71
การแก้ปัญหาที่ให้มีความซับซ้อนโดยไม่จำเป็น ดูโซลูชันของ Marc Gravell หรือโซลูชันของ Zaid Masud หากคุณให้ความสำคัญกับความเรียบง่าย
Frank Hileman

8
วิธีการนี้จะทำให้สิ้นเปลืองทรัพยากรเป็นจำนวนมากหากคุณอัปเดตหลาย ๆ คุณสมบัติเนื่องจาก Invoke ทุกตัวต้องใช้ทรัพยากรจำนวนมาก ฉันไม่คิดว่านี่เป็นลักษณะของ Thread Safety ที่ตั้งใจไว้ Encapsulte การดำเนินการอัปเดต UI ของคุณและเรียกใช้ครั้งเดียว (และไม่ใช่ต่อสถานที่ให้บริการ)
คอนโซล

4
เหตุใดคุณจึงต้องใช้รหัสนี้บนองค์ประกอบ BackgroundWorker
Andy

1080

ง่ายวิธีเป็นวิธีการที่ไม่ระบุชื่อผ่านเข้ามาในLabel.Invoke:

// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
    // Running on the UI thread
    form.Label.Text = newText;
});
// Back on the worker thread

โปรดสังเกตว่าInvokeบล็อกการดำเนินการจนกว่าจะเสร็จสมบูรณ์ - นี่คือรหัสซิงโครนัส คำถามไม่ได้ถามเกี่ยวกับรหัสแบบอะซิงโครนัส แต่มีเนื้อหามากมายใน Stack Overflowเกี่ยวกับการเขียนรหัสแบบอะซิงโครนัสเมื่อคุณต้องการเรียนรู้เกี่ยวกับมัน


8
เห็นเป็น OP ไม่ได้กล่าวถึงระดับใด ๆ / อินสแตนซ์ยกเว้นรูปแบบที่ไม่ได้เป็นค่าเริ่มต้นไม่ดี ...
Marc Gravell

39
อย่าลืมคำหลัก "นี่" กำลังอ้างถึงคลาส "ควบคุม"
อาริโซน่า

8
@codecompleting มันปลอดภัยทั้งสองทางและเรารู้อยู่แล้วว่าเราเป็นพนักงานดังนั้นทำไมต้องตรวจสอบบางสิ่งที่เรารู้
Marc Gravell

4
@Dragouf ไม่จริง - หนึ่งในจุดที่ใช้วิธีนี้คือคุณรู้อยู่แล้วว่าชิ้นส่วนใดที่ทำงานกับผู้ปฏิบัติงานและทำงานบนเธรด UI ไม่จำเป็นต้องตรวจสอบ
Marc Gravell

3
@ Joan.bdm ไม่มีบริบทเพียงพอที่จะให้ฉันแสดงความคิดเห็นได้
Marc Gravell

401

การจัดการงานที่ยาวนาน

ตั้งแต่. NET 4.5 และ C # 5.0คุณควรใช้Asynchronous Pattern (TAP)ควบคู่ไปกับasync - รอคำสำคัญในทุกพื้นที่ (รวมถึง GUI):

TAP เป็นรูปแบบการออกแบบอะซิงโครนัสที่แนะนำสำหรับการพัฒนาใหม่

แทนที่จะเป็นAsynchronous Programming Model (APM)และAsynchronous Pattern (EAP) แบบอิงเหตุการณ์ (ซึ่งในภายหลังจะมีBackgroundWorker Class )

จากนั้นโซลูชันที่แนะนำสำหรับการพัฒนาใหม่คือ:

  1. การใช้งานตัวจัดการเหตุการณ์แบบอะซิงโครนัส (ใช่นั่นคือทั้งหมด):

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var progress = new Progress<string>(s => label.Text = s);
        await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
                                    TaskCreationOptions.LongRunning);
        label.Text = "completed";
    }
  2. การใช้เธรดที่สองที่แจ้งเธรด UI:

    class SecondThreadConcern
    {
        public static void LongWork(IProgress<string> progress)
        {
            // Perform a long running work...
            for (var i = 0; i < 10; i++)
            {
                Task.Delay(500).Wait();
                progress.Report(i.ToString());
            }
        }
    }

แจ้งให้ทราบดังต่อไปนี้:

  1. รหัสสั้นและสะอาดเขียนตามลำดับโดยไม่มีการเรียกกลับและเธรดที่ชัดเจน
  2. งานแทนกระทู้
  3. คำหลักasyncที่อนุญาตให้ใช้รอซึ่งจะป้องกันไม่ให้ตัวจัดการเหตุการณ์เข้าถึงสถานะเสร็จสิ้นจนกว่างานจะเสร็จสิ้นและในขณะเดียวกันก็ไม่ได้ปิดกั้นเธรด UI
  4. คลาส Progress (ดูIProgress Interface ) ที่รองรับหลักการแยกการออกแบบ(SoC)และไม่ต้องการดิสแพดเชอร์และเรียกใช้อย่างชัดเจน มันใช้SynchronizationContextปัจจุบันจากสถานที่สร้างของมัน (นี่ด้าย UI)
  5. TaskCreationOptions.LongRunningคำแนะนำว่าจะทำไม่ได้คิวงานเข้าสู่ThreadPool

สำหรับตัวอย่างที่ละเอียดยิ่งกว่าดูที่: อนาคตของ C #: สิ่งดีๆมาถึงผู้ที่ 'รอคอย'โดยโจเซฟอัลบาฮารี

ดูเพิ่มเติมเกี่ยวกับแนวคิดUI Threading Model

จัดการข้อยกเว้น

ตัวอย่างด้านล่างเป็นตัวอย่างของวิธีจัดการกับข้อยกเว้นและEnabledคุณสมบัติของปุ่มสลับเพื่อป้องกันไม่ให้มีการคลิกหลายครั้งระหว่างการดำเนินการพื้นหลัง

private async void Button_Click(object sender, EventArgs e)
{
    button.Enabled = false;

    try
    {
        var progress = new Progress<string>(s => button.Text = s);
        await Task.Run(() => SecondThreadConcern.FailingWork(progress));
        button.Text = "Completed";
    }
    catch(Exception exception)
    {
        button.Text = "Failed: " + exception.Message;
    }

    button.Enabled = true;
}

class SecondThreadConcern
{
    public static void FailingWork(IProgress<string> progress)
    {
        progress.Report("I will fail in...");
        Task.Delay(500).Wait();

        for (var i = 0; i < 3; i++)
        {
            progress.Report((3 - i).ToString());
            Task.Delay(500).Wait();
        }

        throw new Exception("Oops...");
    }
}

2
หากSecondThreadConcern.LongWork()มีข้อผิดพลาดสามารถใช้เธรด UI ได้หรือไม่ นี่คือโพสต์ที่ยอดเยี่ยม btw
kdbanman

2
ฉันได้เพิ่มส่วนเพิ่มเติมในคำตอบเพื่อตอบสนองความต้องการของคุณ ความนับถือ.
Ryszard Dżegan

3
ระดับ ExceptionDispatchInfoเป็นผู้รับผิดชอบต่อความมหัศจรรย์ของ rethrowing ยกเว้นพื้นหลังในหัวข้อ UI ในรูปแบบ async-รอคอย
Ryszard Dżegan

1
ฉันแค่คิดว่าวิธีการทำเช่นนี้เป็นวิธีที่ละเอียดกว่าการเรียกใช้ Invoke / Begin!
MeTitus

2
Task.Delay(500).Wait()? จุดประสงค์ของการสร้างภารกิจเพื่อบล็อกเธรดปัจจุบันคืออะไร คุณไม่ควรบล็อกเธรดพูลเธรด!
Yarik

236

การเปลี่ยนแปลงของโซลูชันที่ง่ายที่สุดของMarc Gravellสำหรับ. NET 4:

control.Invoke((MethodInvoker) (() => control.Text = "new text"));

หรือใช้ผู้แทนการดำเนินการแทน:

control.Invoke(new Action(() => control.Text = "new text"));

ดูที่นี่สำหรับการเปรียบเทียบของทั้งสอง: MethodInvoker vs Action สำหรับ Control.BeginInvoke


1
'การควบคุม' ในตัวอย่างนี้คืออะไร การควบคุม UI ของฉัน? พยายามใช้สิ่งนี้ใน WPF บนตัวควบคุมป้ายกำกับและ Invoke ไม่ใช่สมาชิกของป้ายกำกับของฉัน
Dbloom

วิธีการเกี่ยวกับส่วนขยายเช่น @styxriver stackoverflow.com/a/3588137/206730 เป็นอย่างไร
Kiquenet

ประกาศ 'การกระทำ y;' ภายในชั้นเรียนหรือวิธีการเปลี่ยนคุณสมบัติข้อความและอัปเดตข้อความด้วยรหัสชิ้นส่วน 'yourcontrol.Invoke (y = () => yourcontrol.Text = "ข้อความใหม่");'
Antonio Leite

4
@bloom มันไม่ได้เป็นสมาชิกเพราะมันมีไว้สำหรับ WinForms เท่านั้น สำหรับ WPF คุณใช้ Dispatcher.Invoke
sLw

1
ฉันกำลังติดตามโซลูชันนี้ แต่บางครั้ง UI ของฉันไม่ได้รับการอัปเดต ฉันพบว่าฉันต้องthis.refresh()บังคับให้โมฆะและทาสี GUI .. ถ้ามันมีประโยชน์ ..
Rakibul Haq

137

ดับและลืมวิธีการขยายสำหรับ. NET 3.5+

using System;
using System.Windows.Forms;

public static class ControlExtensions
{
    /// <summary>
    /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
    /// </summary>
    /// <param name="control"></param>
    /// <param name="code"></param>
    public static void UIThread(this Control @this, Action code)
    {
        if (@this.InvokeRequired)
        {
            @this.BeginInvoke(code);
        }
        else
        {
            code.Invoke();
        }
    }
}

สามารถเรียกได้ว่าใช้รหัสบรรทัดต่อไปนี้:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");

5
จุดประสงค์ของการใช้ @this คืออะไร "การควบคุม" จะไม่เทียบเท่าหรือไม่ @this นี้มีประโยชน์อะไรบ้าง?
argyle

14
@ jeromeyers - @thisเป็นเพียงชื่อตัวแปรในกรณีนี้อ้างอิงถึงการควบคุมปัจจุบันเรียกส่วนขยาย คุณสามารถเปลี่ยนชื่อมันเป็นแหล่งที่มาหรือสิ่งที่ลอยเรือของคุณ ฉันใช้@thisเพราะหมายถึง 'การควบคุมนี้' ที่เรียกส่วนขยายและสอดคล้อง (ในหัวของฉันอย่างน้อย) โดยใช้คำหลัก 'this' ในรหัสปกติ (ไม่ใช่ส่วนขยาย)
StyxRiver

1
นี่เป็นวิธีที่ดีง่ายและสำหรับฉันทางออกที่ดีที่สุด คุณสามารถรวมงานทั้งหมดที่คุณต้องทำในกระทู้ ui ตัวอย่าง: this.UIThread (() => {txtMessage.Text = message; listBox1.Items.Add (ข้อความ);});
อัตโนมัติ

1
ฉันชอบวิธีนี้มาก จู้จี้ไมเนอร์: ฉันจะตั้งชื่อวิธีนี้มากกว่าOnUIThread UIThread
ToolmakerSteve

2
นั่นเป็นเหตุผลที่ฉันตั้งชื่อส่วนขยายRunOnUiThreadนี้ แต่นั่นเป็นเพียงรสนิยมส่วนตัว
Grisgram

66

นี่คือวิธีคลาสสิกที่คุณควรทำ:

using System;
using System.Windows.Forms;
using System.Threading;

namespace Test
{
    public partial class UIThread : Form
    {
        Worker worker;

        Thread workerThread;

        public UIThread()
        {
            InitializeComponent();

            worker = new Worker();
            worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
            workerThread = new Thread(new ThreadStart(worker.StartWork));
            workerThread.Start();
        }

        private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
        {
            // Cross thread - so you don't get the cross-threading exception
            if (this.InvokeRequired)
            {
                this.BeginInvoke((MethodInvoker)delegate
                {
                    OnWorkerProgressChanged(sender, e);
                });
                return;
            }

            // Change control
            this.label1.Text = e.Progress;
        }
    }

    public class Worker
    {
        public event EventHandler<ProgressChangedArgs> ProgressChanged;

        protected void OnProgressChanged(ProgressChangedArgs e)
        {
            if(ProgressChanged!=null)
            {
                ProgressChanged(this,e);
            }
        }

        public void StartWork()
        {
            Thread.Sleep(100);
            OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
            Thread.Sleep(100);
        }
    }


    public class ProgressChangedArgs : EventArgs
    {
        public string Progress {get;private set;}
        public ProgressChangedArgs(string progress)
        {
            Progress = progress;
        }
    }
}

เธรดคนทำงานของคุณมีเหตุการณ์ เธรด UI ของคุณเริ่มต้นเธรดอื่นเพื่อทำงานและเชื่อมต่อเหตุการณ์ของผู้ปฏิบัติงานนั้นเพื่อให้คุณสามารถแสดงสถานะของเธรดผู้ปฏิบัติงาน

จากนั้นใน UI คุณต้องข้ามเธรดเพื่อเปลี่ยนการควบคุมจริง ... เช่นป้ายกำกับหรือแถบความคืบหน้า


62

Control.Invokeวิธีการแก้ปัญหาที่ง่ายคือการใช้งาน

void DoSomething()
{
    if (InvokeRequired) {
        Invoke(new MethodInvoker(updateGUI));
    } else {
        // Do Something
        updateGUI();
    }
}

void updateGUI() {
    // update gui here
}

ทำได้ดีมากสำหรับความเรียบง่าย! ไม่เพียง แต่เรียบง่าย แต่ยังทำงานได้ดี! ฉันไม่เข้าใจจริงๆว่าทำไมไมโครซอฟท์ไม่สามารถทำให้มันง่ายขึ้นได้ตามที่ตั้งใจ! สำหรับการเรียก 1 บรรทัดบนเธรดหลักเราควรเขียนฟังก์ชั่นสองอย่าง!
MBH

1
@MBH เห็นด้วย BTW คุณสังเกตเห็นstackoverflow.com/a/3588137/199364คำตอบข้างต้นซึ่งกำหนดวิธีการขยายหรือไม่ อย่าว่าเมื่อในชั้นเรียนสาธารณูปโภคที่กำหนดเองแล้วไม่ต้องดูแลใด ๆ เพิ่มเติมที่ไมโครซอฟท์ไม่ได้ทำมันสำหรับเรา :)
ToolmakerSteve

@ToolmakerSteve นั่นคือความหมายของมัน! คุณอยู่ที่ถูกต้องเราสามารถหาวิธี แต่ฉันหมายถึงจาก DRY (ไม่ทำซ้ำตัวเอง) มุมมองปัญหาที่มีวิธีแก้ปัญหาที่พบบ่อยสามารถแก้ไขได้โดยพวกเขาด้วยความพยายามขั้นต่ำโดย Microsoft ซึ่งจะช่วยประหยัดเวลาได้มาก โปรแกรมเมอร์) :)
MBH

47

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

    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.WorkerReportsProgress = true;
        backgroundWorker1.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "A");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "B");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "C");
    }

    private void backgroundWorker1_ProgressChanged(
        object sender, 
        ProgressChangedEventArgs e)
    {
        label1.Text = e.UserState.ToString();
    }

ไม่เป็นไรถ้าคุณต้องการอัปเดตฟิลด์เดิมเสมอ หากคุณมีการอัปเดตที่ซับซ้อนกว่านี้คุณสามารถกำหนดคลาสเพื่อแสดงสถานะ UI และส่งต่อไปยังเมธอด ReportProgress

สิ่งสุดท้ายที่แน่นอนอย่าลืมตั้งค่าWorkerReportsProgressสถานะมิฉะนั้นReportProgressวิธีการจะถูกละเว้นอย่างสมบูรณ์


2
backgroundWorker1_RunWorkerCompletedในตอนท้ายของการประมวลผลก็ยังเป็นไปได้ที่จะปรับปรุงส่วนติดต่อผู้ใช้ผ่านทาง
DavidRR

41

ส่วนใหญ่ของการใช้งานตอบControl.Invokeซึ่งเป็นสภาพการแข่งขันที่จะเกิดขึ้นรอ ตัวอย่างเช่นพิจารณาคำตอบที่ยอมรับได้:

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

หากผู้ใช้ปิดแบบฟอร์มก่อนหน้าthis.Invokeนี้เรียกว่า (จำไว้ว่าthisเป็นFormวัตถุ) ObjectDisposedExceptionจะมีแนวโน้มที่จะยิง

การแก้ปัญหาคือการใช้SynchronizationContextโดยเฉพาะอย่างยิ่งSynchronizationContext.Currentเป็นhamilton.danielbแนะนำ (คำตอบอื่น ๆ ขึ้นอยู่กับSynchronizationContextการใช้งานเฉพาะที่ไม่จำเป็นอย่างสมบูรณ์) ฉันจะปรับเปลี่ยนรหัสของเขาเล็กน้อยเพื่อใช้SynchronizationContext.Postมากกว่าSynchronizationContext.Send(เนื่องจากโดยทั่วไปแล้วไม่จำเป็นต้องรอเธรดของผู้ปฏิบัติงาน):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        ...
    }

    private MethodOnOtherThread()
    {
         ...
         _context.Post(status => someLabel.Text = newText,null);
    }
}

โปรดทราบว่าใน. NET 4.0 ขึ้นไปคุณควรใช้งานจริงสำหรับการดำเนินการ async ดูคำตอบของ n-sanสำหรับวิธีการทำงานแบบเทียบเท่า (โดยใช้TaskScheduler.FromCurrentSynchronizationContext)

ในที่สุดบน. NET 4.5 และสูงกว่าคุณสามารถใช้Progress<T>(ซึ่งโดยทั่วไปจะยึดSynchronizationContext.Currentตามการสร้างของมัน) ดังที่Ryszard Dżeganแสดงให้เห็นในกรณีที่การดำเนินการที่ต้องใช้เวลานานในการรันโค้ด UI ในขณะที่ยังทำงานอยู่


37

คุณจะต้องตรวจสอบให้แน่ใจว่าการอัปเดตเกิดขึ้นในเธรดที่ถูกต้อง เธรด UI

ในการดำเนินการนี้คุณจะต้องเรียกใช้ตัวจัดการเหตุการณ์แทนที่จะเรียกใช้โดยตรง

คุณสามารถทำได้โดยเพิ่มกิจกรรมของคุณเช่นนี้:

(รหัสนี้พิมพ์ออกมาจากหัวของฉันดังนั้นฉันจึงไม่ได้ตรวจสอบไวยากรณ์ที่ถูกต้อง ฯลฯ แต่คุณควรจะไป)

if( MyEvent != null )
{
   Delegate[] eventHandlers = MyEvent.GetInvocationList();

   foreach( Delegate d in eventHandlers )
   {
      // Check whether the target of the delegate implements 
      // ISynchronizeInvoke (Winforms controls do), and see
      // if a context-switch is required.
      ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

      if( target != null && target.InvokeRequired )
      {
         target.Invoke (d, ... );
      }
      else
      {
          d.DynamicInvoke ( ... );
      }
   }
}

โปรดทราบว่ารหัสข้างต้นจะไม่ทำงานในโครงการ WPF เนื่องจากการควบคุม WPF ไม่ได้ใช้ISynchronizeInvokeอินเตอร์เฟซ

ในการสั่งซื้อเพื่อให้แน่ใจว่ารหัสดังกล่าวทำงานร่วมกับ Windows แบบฟอร์มและ WPF และแพลตฟอร์มอื่น ๆ ทั้งหมดที่คุณสามารถดูได้ที่ได้AsyncOperation, AsyncOperationManagerและSynchronizationContextชั้นเรียน

เพื่อให้ง่ายต่อการเพิ่มกิจกรรมด้วยวิธีนี้ฉันได้สร้างวิธีการขยายซึ่งทำให้ฉันสามารถเพิ่มกิจกรรมให้ง่ายขึ้นเพียงแค่โทร:

MyEvent.Raise(this, EventArgs.Empty);

แน่นอนคุณสามารถใช้คลาส BackGroundWorker ซึ่งจะทำให้เรื่องนี้เป็นนามธรรมของคุณ


แน่นอน แต่ฉันไม่ชอบ 'ยุ่ง' รหัส GUI ของฉันกับเรื่องนี้ GUI ของฉันไม่ควรสนใจว่าจะต้องเรียกใช้หรือไม่ ในคำอื่น ๆ : ฉันไม่คิดว่ามันเป็นความรับผิดชอบของ GUI เพื่อดำเนินการบริบท -withc
Frederik Gheysels

1
การแบ่งผู้รับมอบสิทธิ์ออกจากกัน ฯลฯ ดูเหมือนจะเกินความเป็นจริง - ทำไมไม่เพียงแค่: SynchronizationContext.Current.Send (ผู้รับมอบสิทธิ์ {MyEvent (... );}, null);
Marc Gravell

คุณมีสิทธิ์เข้าถึง SynchronizationContext เสมอหรือไม่ แม้ว่าชั้นเรียนของคุณจะอยู่ในคลาส lib หรือไม่?
Frederik Gheysels

29

คุณจะต้องเรียกใช้เมธอดบนเธรด GUI คุณสามารถทำได้โดยโทร Control.Invoke

ตัวอย่างเช่น:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}

1
บรรทัดที่เรียกใช้ทำให้ฉันมีข้อผิดพลาดของคอมไพเลอร์ ที่ดีที่สุดตรงกับวิธีการมากเกินไปสำหรับ 'System.Windows.Forms.Control.Invoke (System.Delegate วัตถุ [])' มีข้อโต้แย้งที่ไม่ถูกต้องบาง
CruelIO

28

เนื่องจากเรื่องไม่สำคัญของสถานการณ์ฉันจะมีแบบสำรวจความคิดเห็นเธรด UI สำหรับสถานะ ฉันคิดว่าคุณจะพบว่ามันค่อนข้างหรูหรา

public class MyForm : Form
{
  private volatile string m_Text = "";
  private System.Timers.Timer m_Timer;

  private MyForm()
  {
    m_Timer = new System.Timers.Timer();
    m_Timer.SynchronizingObject = this;
    m_Timer.Interval = 1000;
    m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
    m_Timer.Start();
    var thread = new Thread(WorkerThread);
    thread.Start();
  }

  private void WorkerThread()
  {
    while (...)
    {
      // Periodically publish progress information.
      m_Text = "Still working...";
    }
  }
}

วิธีการนี้จะช่วยหลีกเลี่ยงการปฏิบัติการที่จำเป็นเมื่อใช้ISynchronizeInvoke.InvokeและISynchronizeInvoke.BeginInvokeวิธีการ ไม่มีอะไรผิดปกติกับการใช้เทคนิคการทำมาร์ชชิ่ง แต่มีข้อควรระวังบางประการที่คุณต้องระวัง

  • ตรวจสอบให้แน่ใจว่าคุณไม่ได้โทรBeginInvokeบ่อยเกินไปหรืออาจทำให้ปั๊มข้อความมากเกินไป
  • การโทรInvokeบนเธรดผู้ปฏิบัติงานเป็นการบล็อกการโทร มันจะหยุดงานที่ทำในเธรดนั้นชั่วคราว

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

  • UI และเธรดผู้ปฏิบัติงานยังคงอยู่คู่กันอย่างหลวม ๆ เมื่อเทียบกับControl.InvokeหรือControl.BeginInvokeวิธีการที่คู่กันแน่น
  • เธรด UI จะไม่ขัดขวางความคืบหน้าของเธรดผู้ปฏิบัติงาน
  • เธรดผู้ปฏิบัติงานไม่สามารถควบคุมเวลาที่เธรด UI ใช้การอัปเดต
  • ช่วงเวลาที่ UI และเธรดผู้ปฏิบัติงานสามารถดำเนินการได้โดยอิสระ
  • เธรดผู้ปฏิบัติงานไม่สามารถเอาชนะปั๊มข้อความของเธรด UI
  • เธรด UI ได้รับการกำหนดเวลาและความถี่ที่ได้รับการปรับปรุง UI

3
ความคิดที่ดี. สิ่งเดียวที่คุณไม่ได้กล่าวถึงคือวิธีกำจัดตัวจับเวลาอย่างถูกต้องเมื่อ WorkerThread เสร็จสิ้น หมายเหตุสิ่งนี้อาจทำให้เกิดปัญหาเมื่อแอปพลิเคชันสิ้นสุดลง (เช่นผู้ใช้ปิดแอปพลิเคชัน) คุณมีความคิดวิธีแก้ปัญหานี้หรือไม่?
Matt

@ Matt แทนการใช้การจัดการไม่ระบุชื่อสำหรับElapsedเหตุการณ์คุณใช้วิธีการของสมาชิกเพื่อให้คุณสามารถลบจับเวลาเมื่อฟอร์มถูกทิ้ง ...
Phil1970

@ Phil1970 - จุดดี คุณหมายถึงชอบSystem.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };และมอบหมายมันผ่านm_Timer.Elapsed += handler;ในภายหลังในบริบทของการกำจัดm_Timer.Elapsed -= handler;ฉันทำถูกไหม? และสำหรับการกำจัด / ปิดทำตามคำแนะนำตามที่กล่าวไว้ที่นี่
Matt

27

ไม่มีสิ่งที่เรียกใช้ในคำตอบก่อนหน้านี้เป็นสิ่งที่จำเป็น

คุณต้องดูที่ WindowsFormsSynchronizationContext:

// In the main thread
WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();

...

// In some non-UI Thread

// Causes an update in the GUI thread.
mUiContext.Post(UpdateGUI, userData);

...

void UpdateGUI(object userData)
{
    // Update your GUI controls here
}

4
คุณคิดว่าวิธีการโพสต์ใช้ภายใต้ประทุนอย่างไร :)
เหลือเชื่อ

23

หนึ่งในนี้จะคล้ายกับวิธีการแก้ปัญหาข้างต้นโดยใช้ NET Framework 3.0 แต่การแก้ไขปัญหาของการสนับสนุนความปลอดภัยเวลารวบรวม

public  static class ControlExtension
{
    delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);

    public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
    {
        if (source.InvokeRequired)
        {
            var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
            source.Invoke(del, new object[]{ source, selector, value});
        }
        else
        {
            var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
            propInfo.SetValue(source, value, null);
        }
    }
}

ใช้:

this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);

คอมไพเลอร์จะล้มเหลวหากผู้ใช้ผ่านชนิดข้อมูลที่ไม่ถูกต้อง

this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");

23

Salvete! เมื่อค้นหาคำถามนี้ฉันพบคำตอบโดยFrankGและOregon Ghostว่ามีประโยชน์ที่สุดสำหรับฉันที่สุด ตอนนี้ฉันโค้ดใน Visual Basic และเรียกใช้ตัวอย่างนี้ผ่านตัวแปลง ดังนั้นฉันไม่แน่ใจว่ามันจะเปิดออก

ฉันมีรูปแบบการโต้ตอบที่เรียกว่าform_Diagnostics,มีกล่อง richtext เรียกว่าupdateDiagWindow,ฉันกำลังใช้เป็นจอแสดงผลการบันทึกประเภท ฉันต้องสามารถอัปเดตข้อความจากกระทู้ทั้งหมด บรรทัดพิเศษช่วยให้หน้าต่างเลื่อนไปยังบรรทัดใหม่ล่าสุดโดยอัตโนมัติ

ดังนั้นตอนนี้ฉันสามารถอัปเดตจอแสดงผลด้วยบรรทัดเดียวจากที่ใดก็ได้ในโปรแกรมทั้งหมดในลักษณะที่คุณคิดว่ามันจะทำงานโดยไม่ต้องมีเธรด:

  form_Diagnostics.updateDiagWindow(whatmessage);

รหัสหลัก (ใส่รหัสนี้ลงในรหัสชั้นเรียนของแบบฟอร์มของคุณ):

#region "---------Update Diag Window Text------------------------------------"
// This sub allows the diag window to be updated by all threads
public void updateDiagWindow(string whatmessage)
{
    var _with1 = diagwindow;
    if (_with1.InvokeRequired) {
        _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
    } else {
        UpdateDiag(whatmessage);
    }
}
// This next line makes the private UpdateDiagWindow available to all threads
private delegate void UpdateDiagDelegate(string whatmessage);
private void UpdateDiag(string whatmessage)
{
    var _with2 = diagwindow;
    _with2.appendtext(whatmessage);
    _with2.SelectionStart = _with2.Text.Length;
    _with2.ScrollToCaret();
}
#endregion

21

สำหรับวัตถุประสงค์มากมายมันง่ายอย่างนี้:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

"serviceGUI ()" เป็นวิธีการระดับ GUI ภายในแบบฟอร์ม (นี่) ที่สามารถเปลี่ยนการควบคุมได้มากเท่าที่คุณต้องการ โทรหา "updateGUI ()" จากเธรดอื่น สามารถเพิ่มพารามิเตอร์เพื่อส่งผ่านค่าหรือ (อาจเร็วกว่า) ใช้ตัวแปรขอบเขตคลาสพร้อมกับล็อกตามต้องการถ้ามีความเป็นไปได้ของการขัดแย้งกันระหว่างเธรดที่เข้าถึงพวกเขาที่อาจทำให้เกิดความไม่แน่นอน ใช้ BeginInvoke แทนการเรียกใช้ถ้าเธรดที่ไม่ใช่ GUI มีความสำคัญต่อเวลา (เก็บคำเตือนของ Brian Gideon ไว้ในใจ)


21

สิ่งนี้ในรูปแบบ C # 3.0 ของฉันสำหรับโซลูชันของ Ian Kemp:

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

คุณเรียกว่าแบบนี้:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. มันจะเพิ่มการตรวจสอบที่ไม่มีผลกับผลลัพธ์ของ "as MemberExpression"
  2. มันช่วยเพิ่มความปลอดภัยประเภทคงที่

มิฉะนั้นต้นฉบับเป็นทางออกที่ดีมาก


21
Label lblText; //initialized elsewhere

void AssignLabel(string text)
{
   if (InvokeRequired)
   {
      BeginInvoke((Action<string>)AssignLabel, text);
      return;
   }

   lblText.Text = text;           
}

โปรดทราบว่าBeginInvoke()เป็นที่ต้องการมากกว่าInvoke()เพราะมีโอกาสน้อยที่จะทำให้เกิดการหยุดชะงัก (อย่างไรก็ตามนี่ไม่ใช่ปัญหาที่นี่เมื่อเพิ่งกำหนดข้อความให้กับป้ายกำกับ):

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

สิ่งนี้ทำให้ซอฟต์แวร์ที่เราวางจำหน่ายบางส่วนหยุดทำงานจริง มันก็มากพอที่ง่ายต่อการแก้ไขโดยการแทนที่ด้วยInvoke() ถ้าคุณมีความจำเป็นสำหรับการดำเนินการซิงโครซึ่งอาจจะเป็นกรณีที่ถ้าคุณต้องการค่าตอบแทนการใช้งานBeginInvoke()BeginInvoke()


20

เมื่อฉันพบปัญหาเดียวกันฉันขอความช่วยเหลือจาก Google แต่แทนที่จะให้วิธีแก้ปัญหาง่ายๆให้ฉันมันทำให้ฉันสับสนมากขึ้นโดยให้ตัวอย่างMethodInvokerและ blah blah blah ดังนั้นฉันตัดสินใจที่จะแก้ปัญหาด้วยตัวเอง นี่คือทางออกของฉัน:

สร้างผู้แทนเช่นนี้

Public delegate void LabelDelegate(string s);

void Updatelabel(string text)
{
   if (label.InvokeRequired)
   {
       LabelDelegate LDEL = new LabelDelegate(Updatelabel);
       label.Invoke(LDEL, text);
   }
   else
       label.Text = text
}

คุณสามารถเรียกใช้ฟังก์ชันนี้ในเธรดใหม่เช่นนี้

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();

Thread(() => .....)อย่าสับสนกับ ฉันใช้ฟังก์ชันนิรนามหรือนิพจน์แลมบ์ดาเมื่อฉันทำงานกับเธรด เพื่อลดบรรทัดของรหัสคุณสามารถใช้ThreadStart(..)วิธีที่ฉันไม่ควรอธิบายที่นี่


17

ใช้สิ่งนี้:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });

หากคุณมีคุณยังe.ProgressPercentageไม่ได้อยู่ในเธรด UI จากวิธีที่คุณเรียกใช้หรือไม่
LarsTech

กระบวนการ ProgressChanged ทำงานบนเธรด UI นี่เป็นหนึ่งในความสะดวกสบายในการใช้ BackgroundWorker เหตุการณ์ที่เสร็จสมบูรณ์แล้วจะรันบน gui ด้วยเช่นกัน สิ่งเดียวที่ทำงานในเธรดที่ไม่ใช่ UI คือเมธอด DoWork
LarsTech

15

คุณสามารถใช้ผู้รับมอบสิทธิ์ที่มีอยู่แล้วAction:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}

14

รุ่นของฉันคือการแทรก"มนต์" บรรทัดซ้ำหนึ่งบรรทัด :

โดยไม่มีข้อโต้แย้ง:

    void Aaaaaaa()
    {
        if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra

        // Your code!
    }

สำหรับฟังก์ชันที่มีอาร์กิวเมนต์:

    void Bbb(int x, string text)
    {
        if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
        // Your code!
    }

นั่นคือไอที


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

ดังที่คุณเห็นif(InvokeRequired) {something long}คุณเพิ่งรู้ "ฟังก์ชั่นนี้ปลอดภัยในการโทรจากเธรดอื่น"


13

ลองรีเฟรชฉลากโดยใช้สิ่งนี้

public static class ExtensionMethods
{
    private static Action EmptyDelegate = delegate() { };

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}

สำหรับWindows Formsหรือไม่
Kiquenet

13

สร้างตัวแปรคลาส:

SynchronizationContext _context;

ตั้งไว้ในตัวสร้างที่สร้าง UI ของคุณ:

var _context = SynchronizationContext.Current;

เมื่อคุณต้องการอัปเดตฉลาก:

_context.Send(status =>{
    // UPDATE LABEL
}, null);

12

คุณต้องใช้การเรียกใช้และการมอบสิทธิ์

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });

12

คำตอบอื่น ๆ ส่วนใหญ่มีความซับซ้อนเล็กน้อยสำหรับฉันในคำถามนี้ (ฉันใหม่กับ C #) ดังนั้นฉันจึงเขียนของฉัน:

ฉันมีแอปพลิเคชันWPFและได้กำหนดผู้ปฏิบัติงานดังนี้:

ปัญหา:

BackgroundWorker workerAllocator;
workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1) {
    // This is my DoWork function.
    // It is given as an anonymous function, instead of a separate DoWork function

    // I need to update a message to textbox (txtLog) from this thread function

    // Want to write below line, to update UI
    txt.Text = "my message"

    // But it fails with:
    //  'System.InvalidOperationException':
    //  "The calling thread cannot access this object because a different thread owns it"
}

สารละลาย:

workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1)
{
    // The below single line works
    txtLog.Dispatcher.BeginInvoke((Action)(() => txtLog.Text = "my message"));
}

ฉันยังไม่ทราบว่าบรรทัดข้างต้นหมายถึงอะไร แต่ก็ใช้งานได้

สำหรับWinForms :

สารละลาย:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});

คำถามเกี่ยวกับ Winform ไม่ใช่ WPF
Marc L.

ขอบคุณ เพิ่มโซลูชัน WinForms ด้านบน
Manohar Reddy Poreddy

... ซึ่งเป็นเพียงสำเนาของคำตอบอื่น ๆ อีกมากมาย แต่คำถามเดียวกันนี้ก็โอเค ทำไมไม่เป็นส่วนหนึ่งของโซลูชันและเพียงแค่ลบคำตอบของคุณ?
Marc L.

อืมถูกต้องคุณเป็นถ้าเพียงคุณอ่านคำตอบของฉันด้วยความสนใจส่วนเริ่มต้น (เหตุผลที่ฉันเขียนคำตอบ) และหวังว่าด้วยความสนใจมากกว่าที่คุณเห็นมีคนที่มีปัญหาเดียวกัน & upvoted วันนี้สำหรับ คำตอบง่าย ๆ ของฉันและถ้าคุณสามารถคาดการณ์เรื่องราวจริง ๆ ได้ว่าทำไมสิ่งทั้งหมดนี้เกิดขึ้น Google ก็ส่งฉันมาที่นี่แม้ว่าฉันจะค้นหา WPF แน่นอนเพราะคุณพลาดเหตุผล 3 ข้อเหล่านี้ไปมากหรือน้อยฉันสามารถเข้าใจได้ว่าทำไมคุณไม่ลบ downvote ของคุณ แทนที่จะทำความสะอาดสิ่งที่ถูกต้องให้สร้างสิ่งใหม่ที่ยากกว่า
Manohar Reddy Poreddy


8

ตัวอย่างเช่นเข้าถึงตัวควบคุมอื่นที่ไม่ใช่เธรดปัจจุบัน:

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

นั่นlblThresholdคือฉลากและSpeed_Thresholdเป็นตัวแปรทั่วโลก


8

เมื่อคุณอยู่ในเธรด UI คุณสามารถขอให้จัดกำหนดการงานบริบทการซิงค์ได้ มันจะให้TaskSchedulerที่กำหนดเวลาทุกอย่างในเธรด UI ของคุณ

จากนั้นคุณสามารถโยงงานของคุณเพื่อให้เมื่อผลลัพธ์พร้อมแล้วงานอื่น (ซึ่งถูกกำหนดเวลาไว้ในเธรด UI) จะเลือกและมอบหมายงานให้กับป้ายกำกับ

public partial class MyForm : Form
{
  private readonly TaskScheduler _uiTaskScheduler;
  public MyForm()
  {
    InitializeComponent();
    _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  }

  private void buttonRunAsyncOperation_Click(object sender, EventArgs e)
  {
    RunAsyncOperation();
  }

  private void RunAsyncOperation()
  {
    var task = new Task<string>(LengthyComputation);
    task.ContinueWith(antecedent =>
                         UpdateResultLabel(antecedent.Result), _uiTaskScheduler);
    task.Start();
  }

  private string LengthyComputation()
  {
    Thread.Sleep(3000);
    return "47";
  }

  private void UpdateResultLabel(string text)
  {
    labelResult.Text = text;
  }
}

งานนี้สำหรับงาน (ไม่ใช่หัวข้อ) ซึ่งเป็นวิธีที่ต้องการของการเขียนรหัสพร้อมกันในขณะนี้


1
Task.Startโดยทั่วไปแล้วการโทรไม่ใช่วิธีปฏิบัติที่ดีblogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider

8

ฉันเพิ่งอ่านคำตอบและสิ่งนี้ดูเหมือนจะเป็นประเด็นร้อน ขณะนี้ฉันใช้. NET 3.5 SP1 และแบบฟอร์ม Windows

สูตรที่รู้จักกันดีอธิบายไว้อย่างมากในคำตอบก่อนหน้าซึ่งใช้คุณสมบัติInvokeRequiredครอบคลุมกรณีส่วนใหญ่ แต่ไม่ใช่กลุ่มทั้งหมด

เกิดอะไรขึ้นถ้าจับไม่ได้รับการสร้างขึ้นหรือยัง

InvokeRequiredคุณสมบัติตามที่อธิบายไว้ที่นี่ (อ้างอิง Control.InvokeRequired คุณสมบัติ MSDN)ผลตอบแทนจริงถ้าโทรที่ถูกสร้างขึ้นจากหัวข้อที่ไม่ด้ายกุยเท็จทั้งหากการเรียกร้องที่ทำจากด้ายกุยหรือถ้าจับเป็น ยังไม่ได้สร้าง

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

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

และผู้รับมอบสิทธิ์สามารถอัปเดต Label บน GUI:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.InvokeRequired)
        _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
    else
        _gui.Label1.Text = "Done!";
}

นี้สามารถทำให้เกิดการInvalidOperationExceptionถ้าการดำเนินงานก่อนการปรับปรุงฉลาก "ใช้เวลาน้อยลง" (อ่านและตีความว่ามันเป็นความเรียบง่ายก) กว่าเวลาที่ใช้สำหรับเธรด GUI ในการสร้างที่แบบฟอร์ม 's จับ สิ่งนี้เกิดขึ้นภายในเมธอดShowDialog ()

คุณควรตรวจสอบที่จับเช่นนี้:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.IsHandleCreated)  //  <---- ADDED
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
}

คุณสามารถจัดการการดำเนินการเพื่อดำเนินการหากยังไม่ได้สร้างจุดจับ : คุณสามารถข้ามการอัปเดต GUI (เช่นที่แสดงในรหัสด้านบน) หรือคุณสามารถรอ (มีความเสี่ยงมากขึ้น) สิ่งนี้ควรตอบคำถาม

สิ่งที่เป็นตัวเลือก: โดยส่วนตัวแล้วฉันคิดรหัสต่อไปนี้:

public class ThreadSafeGuiCommand
{
  private const int SLEEPING_STEP = 100;
  private readonly int _totalTimeout;
  private int _timeout;

  public ThreadSafeGuiCommand(int totalTimeout)
  {
    _totalTimeout = totalTimeout;
  }

  public void Execute(Form form, Action guiCommand)
  {
    _timeout = _totalTimeout;
    while (!form.IsHandleCreated)
    {
      if (_timeout <= 0) return;

      Thread.Sleep(SLEEPING_STEP);
      _timeout -= SLEEPING_STEP;
    }

    if (form.InvokeRequired)
      form.Invoke(guiCommand);
    else
      guiCommand();
  }
}

ฉันป้อนข้อมูลในแบบฟอร์มของฉันที่ได้รับการปรับปรุงโดยเธรดอื่นด้วยอินสแตนซ์ของThreadSafeGuiCommandนี้และฉันกำหนดวิธีการที่ปรับปรุง GUI (ในแบบฟอร์มของฉัน) ดังนี้:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

ด้วยวิธีนี้ฉันค่อนข้างมั่นใจว่าฉันจะได้รับการปรับปรุง GUI ของฉันสิ่งที่ด้ายจะโทรออกหรือเลือกที่จะรอเวลาที่กำหนดไว้อย่างดี (หมดเวลา)


1
มาที่นี่เพื่อค้นหาสิ่งนี้เพราะฉันตรวจสอบ IsHandleCreated ด้วย อีกคุณสมบัติหนึ่งที่ต้องตรวจสอบคือ IsDisposed หากแบบฟอร์มของคุณถูกกำจัดคุณจะไม่สามารถเรียกใช้ Invoke () ได้ หากผู้ใช้ปิดแบบฟอร์มก่อนที่เธรดพื้นหลังของคุณจะเสร็จสมบูรณ์คุณไม่ต้องการให้พยายามโทรกลับไปที่ UI เมื่อมีการจำหน่ายแบบฟอร์ม
Jon

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

สถานการณ์ที่อธิบายไว้จะพิจารณาถึงรูปแบบคำกริยาที่ใช้เป็นมุมมองความคืบหน้าของงานพื้นหลัง - เธรด เนื่องจากต้องเป็นแบบโมดัลจึงต้องแสดงโดยเรียกเมธอดForm.ShowDialog () โดยการทำเช่นนี้คุณป้องกันโค้ดของคุณที่ตามหลังการเรียกที่จะดำเนินการจนกว่าจะปิดแบบฟอร์ม ดังนั้นถ้าคุณไม่สามารถเริ่มด้ายพื้นหลังที่แตกต่างจากตัวอย่างที่กำหนด (และแน่นอนคุณสามารถ) แบบฟอร์มนี้จะต้องแสดงอย่างสุภาพหลังจากเริ่มด้ายพื้นหลัง ในกรณีนี้คุณต้องตรวจสอบว่าจะสร้างมือจับหรือไม่ หากคุณไม่ต้องการรูปแบบเป็นกิริยาช่วยแสดงว่าเป็นอีกเรื่องหนึ่ง
Sume
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.