实际上,你正在中断一个队列,这可能会有很多不可预见的后果。Invoke 实际上是将你想要做的事情以“礼貌”的方式放入该队列中,从 .Net 2.0 开始通过抛出 InvalidOperationException 强制执行此规则。
为了理解幕后正在发生的事情以及所说的“GUI 线程”,了解消息泵或消息循环是有用的。
这个问题在 "什么是消息泵" 中已经得到了回答,阅读此文可更好地理解与控件交互时所涉及的实际机制。
其他有用的阅读材料包括:
Windows GUI 编程的基本规则之一是,只有创建控件的线程才能访问和/或修改其内容(除了一些记录在案的例外情况)。如果从其他线程尝试进行操作,则会出现无法预测的行为,从死锁、异常到 UI 只更新了一半。因此,从另一个线程更新控件的正确方法是向应用程序消息队列中发送适当的消息。当消息泵执行该消息时,控件将在创建它的同一线程上得到更新(请记住,消息泵在主线程上运行)。
对于更多代码示例的概述,请参见:
// the canonical form (C# consumer)
public delegate void ControlStringConsumer(Control control, string text); // defines a delegate type
public void SetText(Control control, string text) {
if (control.InvokeRequired) {
control.Invoke(new ControlStringConsumer(SetText), new object[]{control, text}); // invoking itself
} else {
control.Text=text; // the "functional part", executing only on the main thread
}
}
在Windows Forms中,控件或窗口对象只是一个围绕着一个由句柄(有时称为HWND)标识的Win32窗口的包装器。您对控件执行的大多数操作最终都将导致使用此句柄的Win32 API调用。该句柄归创建它的线程(通常是主线程)所有,不应被另一个线程操纵。如果由于某种原因需要从另一个线程执行控件操作,则可以使用Invoke
要求主线程代表您执行。
例如,如果您想要从工作线程更改标签的文本,可以执行以下操作:
theLabel.Invoke(new Action(() => theLabel.Text = "hello world from worker thread!"));
this.Invoke(() => this.Enabled = true);
无论 this
指的是什么,它肯定在当前线程中,对吧? - Kyle Delaney如果您想修改控件,必须在创建控件的线程中进行。该Invoke
方法允许您在关联线程(拥有控件基础窗口句柄的线程)中执行方法。
在下面的示例中,thread1引发异常,因为SetText1尝试从另一个线程修改textBox1.Text。但是,在thread2中,SetText2中的操作在创建TextBox的线程中执行。
private void btn_Click(object sender, EvenetArgs e)
{
var thread1 = new Thread(SetText1);
var thread2 = new Thread(SetText2);
thread1.Start();
thread2.Start();
}
private void SetText1()
{
textBox1.Text = "Test";
}
private void SetText2()
{
textBox1.Invoke(new Action(() => textBox1.Text = "Test"));
}
Invoke((MethodInvoker)delegate{ textBox1.Text = "Test"; });
实际上,这意味着代理将保证在主线程上被调用。这很重要,因为在窗口控件的情况下,如果您不在主线程上更新它们的属性,则要么看不到更改,要么控件会引发异常。
模式如下:
void OnEvent(object sender, EventArgs e)
{
if (this.InvokeRequired)
{
this.Invoke(() => this.OnEvent(sender, e);
return;
}
// do stuff (now you know you are on the main thread)
}
this.Invoke(delegate)
确保您在主线程/创建的线程上调用委托作为 this.Invoke()
的参数。
我可以说一个经验法则,除了从主线程访问您的窗体控件之外,不要访问它们。
也许下面的几行代码使用Invoke()会更有意义:
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text = text;
}
}
有时候,即使你创建了一个线程池线程(也就是工作线程),它仍然会在主线程上运行。这是因为主线程可用于处理更多的指令,所以不会创建新的线程。因此,首先要调查当前运行的线程是否为主线程,可以使用 this.InvokeRequired
来判断。如果返回 true,则当前代码正在工作线程上运行,因此需要调用 this.Invoke(d, new object[] { text });
否则,直接更新 UI 控件 (在这里,您可以确保您正在主线程上运行代码)。
int AddFiveToNumber(int number)
{
var d = (int i => i + 5);
d.Invoke(number);
}
这意味着你传递的委托将在创建 Control 对象的线程上执行(即 UI 线程)。
当你的应用程序是多线程的,并且你想从一个非 UI 线程执行一些 UI 操作时,你需要调用此方法,因为如果你尝试从不同的线程调用 Control 上的方法,你会得到一个 System.InvalidOperationException。