提问



从另一个线程更新Label的最简单方法是什么?


thread1上有一个Form,从那里我开始另一个线程[[thread2)。虽然thread2处理一些文件我想更新thread2 thread1 58 [[]]在Form上,当前状态为thread2的工作。


我怎样才能做到这一点?

最佳参考


对于.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和lambda表达式来实现更清晰,更简单和更安全的语法:


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版本好很多。


如果有人对如何为编译时安全性改进此代码有任何进一步的建议,请评论!

其它参考1


最简单方式是传递给Label.Invoke的匿名方法:[115]


// 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上有很多关于编写异步代码的内容。

其它参考2


处理长期工作



从.NET 4.5和C#5.0开始,您应该使用基于任务的异步模式(TAP)以及异步 - 等待关键字 在所有领域 (包括GUI):[117] [118] [119] [120] [121]



  TAP是新开发的推荐异步设计模式



而不是异步编程模型(APM)和基于事件的异步模式(EAP)(后者包括BackgroundWorker类)。[122] [123] [124]


然后,推荐的新开发解决方案是:



  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. 支持Separation of Concerns(SoC)设计原则的Progress class(请参阅IProgress接口),并且不需要显式调度和调用。它使用当前的SynchronizationContext来自其创建位置(此处为UI线程)。

  5. TaskCreationOptions.LongRunning,提示不将任务排入ThreadPool。



有关更详细的例子,请参阅:C#的未来:约瑟夫·阿尔巴哈里等待的人有好的事。[125] [126] [127] [128] [129] [130] [131] [132] [133] [134] [135]


另请参阅UI线程模型概念。


处理异常



下面的代码片段是如何处理异常和切换按钮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...");
    }
}

其它参考3


Marc Gravell对.NET 4的最简单解决方案的变化:


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


或者使用Action委托代替:


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


请参阅此处以获取两者的比较:MethodInvoker与Control.BeginInvoke的行动

其它参考4


.NET 3.5+的Fire and forget扩展方法


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


这是你应该这样做的经典方式:


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中,您需要跨线程来更改实际控件...如标签或进度条。

其它参考6


简单的解决方案是使用Control.Invoke


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

void updateGUI() {
    // update gui here
}

其它参考7


线程代码经常出错并且总是难以测试。您不需要编写线程代码来从后台任务更新用户界面。只需使用BackgroundWorker类来运行任务及其ReportProgress方法来更新用户界面。通常,您只需报告完成百分比,但是另一个包含状态对象的重载。这是一个只报告字符串对象的例子:[139] [140]


    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方法将被完全忽略。

其它参考8


绝大多数答案使用Control.Invoke这是一种等待发生的竞争条件。例如,考虑接受的答案:


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


如果用户在调用this.Invoke之前关闭表单(记住,thisForm对象),则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及更高版本中,您应该使用异步操作的任务。有关基于任务的等效方法,请参阅n-san的答案(使用TaskScheduler.FromCurrentSynchronizationContext)。


最后,在.NET 4.5及更高版本中,您还可以使用Progress<T>(基本上在创建时捕获SynchronizationContext.Current,如RyszardDżegan所证明的那样,长时间运行的操作需要运行UI代码虽然还在工作。

其它参考9


您必须确保更新发生在正确的线程上; UI线程。


为了做到这一点,你必须调用事件处理程序而不是直接调用它。


您可以通过以下方式举办Activity来做到这一点:


(代码在这里打印出来,所以我没有检查正确的语法等,但它应该让你去。)


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以及所有其他平台,您可以查看AsyncOperationAsyncOperationManagerSynchronizationContext类。


为了以这种方式轻松地引发事件,我创建了一个扩展方法,它允许我通过调用简化来简化事件:


MyEvent.Raise(this, EventArgs.Empty);


当然,您也可以使用BackGroundWorker类,它将为您抽象出这个问题。

其它参考10


您需要在GUI线程上调用该方法。您可以通过调用Control.Invoke来实现。


例如:


delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

其它参考11


由于场景的微不足道,我实际上会对状态进行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.InvokeISynchronizeInvoke.BeginInvoke方法时所需的编组操作。使用编组技术没有任何问题,但您需要注意几个警告。



  • 请确保您不要过于频繁地拨打BeginInvoke,否则可能会超出消息泵。

  • 在工作线程上调用Invoke是一个阻塞调用。它将暂时停止在该线程中完成的工作。



我在这个答案中提出的策略颠倒了线程的通信角色。 UI线程轮询它而不是工作线程推送数据。这是许多场景中使用的常见模式。由于您要做的只是显示工作线程的进度信息,我认为您会发现此解决方案是编组解决方案的一个很好的替代方案。它具有以下优点。



  • UI和工作线程保持松散耦合,而不是紧密耦合它们的Control.InvokeControl.BeginInvoke方法。

  • UI线程不会妨碍工作线程的进度。

  • 工作线程无法控制UI线程更新的时间。

  • UI和工作线程执行操作的时间间隔可以保持独立。

  • 工作线程不能超出UI线程的消息泵。

  • UI线程决定了UI更新的时间和频率。


其它参考12


以前的答案中没有必要的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
}

其它参考13


出于许多目的,它就像这样简单:


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


serviceGUI()是表单(this)中的GUI级方法,可以根据需要更改任意数量的控件。从另一个线程调用updateGUI()。可以添加参数来传递值,或者(可能更快)使用带有锁定的类范围变量,如果有可能在访问它们的线程之间发生冲突而导致不稳定。如果非GUI线程是时间关键的,请使用BeginInvoke而不是Invoke(记住Brian Gideon的警告)。

其它参考14


这在Ian Kemp解决方案的C#3.0变体中:


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. 它改善了静态类型安全性。



否则,原始是一个非常好的解决方案。

其它参考15


这个类似于上面使用.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");

其它参考16


Salvete!搜索了这个问题后,我发现 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

其它参考17


Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}


请注意,BeginInvoke()优先于Invoke(),因为它不太可能导致死锁(但是,仅在将文本分配给标签时,这不是问题):


使用Invoke()时,您正在等待返回的方法。现在,可能是你在需要等待线程的调用代码中做了一些事情,如果它隐藏在你正在调用的某些函数中,这可能不会立即明显,这本身可能通过事件处理程序间接发生。所以你会等待线程,线程会等你,你就陷入僵局。


这实际上导致我们发布的一些软件挂起。通过用BeginInvoke()替换Invoke()来修复很容易。除非您需要同步操作(如果需要返回值),否则请使用BeginInvoke()

其它参考18


当我遇到同样的问题时,我向谷歌寻求帮助,但不是给我一个简单的解决方案,而是通过给出MethodInvoker和等等等等的例子让我更加困惑。所以我决定自己解决它。这是我的解决方案:


像这样委托:


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(() => .....)混淆。当我处理一个线程时,我使用匿名函数或lambda表达式。为了减少代码行,你也可以使用ThreadStart(..)方法,我不应该这样做在这里解释。

其它参考19


只需使用以下内容:


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

其它参考20


您可以使用已有的委托Action:


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

其它参考21


我的版本是插入一行的递归咒语:


没有参数:


    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!
    }


这是IT





一些论证:通常,在一行中的if ()语句之后放置{}会导致代码可读性不好。但在这种情况下,这是常规的咒语。如果这个方法在项目中保持一致,它不会破坏代码的可读性。它可以防止你的代码乱扔垃圾(一行代码而不是五行代码)。


如您所见if(InvokeRequired) {something long},您只知道此功能可以安全地从另一个线程调用。

其它参考22


您必须使用invoke和delegate


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

其它参考23


尝试使用此刷新标签


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

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

其它参考24


创建一个类变量:


SynchronizationContext _context;


在创建UI的构造函数中设置它:


var _context = SynchronizationContext.Current;


如果要更新标签:


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

其它参考25


我认为最简单的方法:


   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }

其它参考26


例如,访问当前线程以外的控件:


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


那里lblThreshold是一个标签,Speed_Threshold是一个全局变量。

其它参考27


当您进入UI线程时,您可以询问它的同步上下文任务调度程序。它会为您提供一个TaskScheduler,它可以调度UI线程上的所有内容。[145]


然后,您可以链接您的任务,以便在结果准备好后,另一个任务(在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;
  }
}


这适用于任务(而不是线程),这是现在编写并发代码的首选方法。[146]

其它参考28


我只是阅读了答案,这似乎是一个非常热门的话题。我目前正在使用.NET 3.5 SP1和Windows Forms。


使用 InvokeRequired 属性的前面答案中大量描述的众所周知的公式涵盖了大多数情况,但不包括整个池。


如果还没有创建句柄怎么办?


如此处所述的 InvokeRequired 属性(对MSDN的Control.InvokeRequired属性引用)如果从不是GUI线程的线程进行调用则返回true,如果从GUI进行调用则返回false线程,或者尚未创建句柄[147]


如果您希望另一个线程显示和更新模式表单,则可能会遇到异常。因为您希望以模态方式显示该表单,所以您可以执行以下操作:


private MyForm _gui;

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


委托可以更新GUI上的Label:


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线程创建表格句柄。这发生在 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,可选择等待明确定义的时间(超时)。

其它参考29


我想添加警告,因为我注意到一些简单的解决方案省略了InvokeRequired检查。


我注意到如果你的代码在创建控件的窗口句柄之前执行 (例如在显示表单之前),Invoke会抛出异常。所以我建议在调用InvokeBeginInvoke之前始终检查InvokeRequired