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

  • ฉันFormกำลังทำงานอยู่thread1และจากนั้นฉันเริ่มเธรดอื่น ( thread2)

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

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

ตอบ

สำหรับ .NET 2.0 นี่เป็นโค้ดเล็กๆ น้อยๆ ที่ฉันเขียนซึ่งทำสิ่งที่คุณต้องการอย่างแท้จริง และใช้ได้กับคุณสมบัติใดๆ บน a 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 ||
      [email protected]().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 มาก

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

ง่ายวิธีเป็นวิธีการที่ไม่ระบุชื่อผ่านเข้ามาใน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เกี่ยวกับการเขียนโค้ดแบบอะซิงโครนัสเมื่อคุณต้องการเรียนรู้เกี่ยวกับมัน

รับมืองานยาว

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

TAP is the recommended asynchronous design pattern for new development

แทนAsynchronous Programming Model (APM)และEvent-based 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ที่อนุญาตให้ใช้awaitซึ่งจะป้องกันไม่ให้ตัวจัดการเหตุการณ์ไปถึงสถานะเสร็จสิ้นจนกว่างานจะเสร็จสิ้น และในระหว่างนี้จะไม่บล็อกเธรด UI
  4. คลาส Progress (ดูIProgress Interface ) ที่รองรับหลักการออกแบบSeparation of Concerns (SoC)และไม่ต้องการโปรแกรมเลือกจ่ายงานและเรียกใช้อย่างชัดเจน ใช้SynchronizationContextปัจจุบันจากสถานที่สร้าง (ที่นี่เธรด UI)
  5. TaskCreationOptions.LongRunningคำแนะนำว่าจะทำไม่ได้คิวงานเข้าสู่ThreadPool

สำหรับการขึ้น verbose ตัวอย่างเห็น: อนาคตของ 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...");
    }
}

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

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

หรือใช้ผู้รับมอบสิทธิ์การดำเนินการแทน:

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

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

ยิงและลืมวิธีการขยายสำหรับ .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");

นี่เป็นวิธีคลาสสิกที่คุณควรทำ:

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 คุณต้องข้ามเธรดเพื่อเปลี่ยนการควบคุมจริง... เช่น ป้ายกำกับหรือแถบความคืบหน้า

วิธีแก้ไขง่ายๆ คือการใช้Control.Invoke.

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

void updateGUI() {
    // update gui here
}

รหัสเธรดมักจะมีปัญหาและยากต่อการทดสอบเสมอ คุณไม่จำเป็นต้องเขียนโค้ดเธรดเพื่ออัปเดตอินเทอร์เฟซผู้ใช้จากงานเบื้องหลัง เพียงใช้คลาส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วิธีการจะถูกละเว้นอย่างสมบูรณ์

ส่วนใหญ่ของการใช้งานตอบ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 ในขณะที่ยังคงทำงานอยู่

คุณจะต้องตรวจสอบให้แน่ใจว่าการอัปเดตเกิดขึ้นในเธรดที่ถูกต้อง เธรด 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 Forms และ WPF และแพลตฟอร์มอื่นๆ ทั้งหมด คุณสามารถดูAsyncOperation, AsyncOperationManagerและSynchronizationContextคลาสได้

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

MyEvent.Raise(this, EventArgs.Empty);

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

เนื่องจากสถานการณ์ที่ไม่สำคัญ ฉันจึงจะมีโพลเธรด 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...";
    }
  }
}

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

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

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

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

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

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

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

ไม่จำเป็นต้องมี Invoke ในคำตอบก่อนหน้านี้

คุณต้องดู 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
}

หนึ่งในนี้จะคล้ายกับวิธีการแก้ปัญหาข้างต้นโดยใช้ 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");

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

ฉันมีไดอะล็อกที่เรียกว่าform_Diagnostics,ซึ่งมีกล่องข้อความแบบริชเท็กซ์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
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()ถ้าคุณมีความจำเป็นสำหรับการดำเนินการซิงโครซึ่งอาจจะเป็นกรณีที่ถ้าคุณต้องการค่าตอบแทนการใช้งาน

สำหรับวัตถุประสงค์หลายๆ อย่าง ทำได้ง่ายๆ ดังนี้:

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

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

นี่ในรูปแบบ 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. เพิ่มการตรวจสอบค่า null ให้กับผลลัพธ์ของ "as MemberExpression"
  2. ช่วยเพิ่มความปลอดภัยประเภทคงที่

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

เมื่อฉันพบปัญหาเดียวกัน ฉันขอความช่วยเหลือจาก 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(..)วิธีที่ฉันไม่ควรอธิบายที่นี่

เพียงใช้สิ่งนี้:

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

คำตอบอื่น ๆ ส่วนใหญ่ค่อนข้างซับซ้อนสำหรับฉันในคำถามนี้ (ฉันยังใหม่กับ 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";
});

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

สำหรับไม่มีอาร์กิวเมนต์:

    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 ()คำสั่งในหนึ่งบรรทัด แต่ในกรณีนี้ มันเป็นกิจวัตรประจำ "มนต์" ที่เหมือนกันหมด ไม่ทำลายความสามารถในการอ่านโค้ดหากวิธีนี้สอดคล้องกันในโครงการ และช่วยประหยัดโค้ดของคุณจากการทิ้งขยะ (โค้ดหนึ่งบรรทัดแทนที่จะเป็นห้าบรรทัด)

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

คุณสามารถใช้ตัวแทนที่มีอยู่แล้วAction:

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

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

SynchronizationContext _context;

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

var _context = SynchronizationContext.Current;

เมื่อคุณต้องการอัปเดตป้ายกำกับ:

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

คุณต้องใช้เรียกและมอบหมาย

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

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

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

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

วิธีที่ง่ายที่สุดในแอปพลิเคชัน WPF คือ:

this.Dispatcher.Invoke((Action)(() =>
{
    // This refers to a form in a WPF application 
    val1 = textBox.Text; // Access the UI 
}));

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

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

มีlblThresholdป้ายกำกับและSpeed_Thresholdเป็นตัวแปรส่วนกลาง

เมื่อคุณอยู่ในเธรด 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;
  }
}

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

และยังมีวิธีการขยายการควบคุมทั่วไปอีกวิธีหนึ่ง..

ขั้นแรกให้เพิ่มวิธีการขยายสำหรับวัตถุประเภทControl

public static void InvokeIfRequired<T>(this T c, Action<T> action) where T : Control
{
    if (c.InvokeRequired)
    {
        c.Invoke(new Action(() => action(c)));
    }
    else
    {
        action(c);
    }
}

และเรียกแบบนี้จากเธรดอื่นเพื่อเข้าถึงการควบคุมที่ชื่อ object1 ใน UI-thread:

object1.InvokeIfRequired(c => { c.Visible = true; });
object1.InvokeIfRequired(c => { c.Text = "ABC"; });

..หรือแบบนี้

object1.InvokeIfRequired(c => 
  { 
      c.Text = "ABC";
      c.Visible = true; 
  }
);