谁能解释一下这个链接上写的这个声明
Invoke(Delegate):
在拥有控件的基础窗口句柄的线程上执行指定的委托。
任何人都可以解释这意味着什么(尤其是粗体)我无法清楚地理解它
这个问题的答案在于 C# 控件是如何工作的
Windows 窗体中的控件绑定到特定线程并且不是线程安全的。因此,如果您从不同的线程调用控件的方法,则必须使用控件的调用方法之一将调用编组到正确的线程。此属性可用于确定是否必须调用调用方法,如果您不知道哪个线程拥有控件,这可能很有用。
实际上,Invoke 所做的是确保您正在调用的代码发生在控件“存在”的线程上,从而有效地防止了跨线程异常。
从历史的角度来看,在 .Net 1.1 中,这实际上是允许的。这意味着您可以尝试从任何后台线程在“GUI”线程上执行代码,这主要是可行的。有时它只会导致您的应用程序退出,因为您在执行其他操作时有效地中断了 GUI 线程。这是跨线程异常- 想象在 GUI 绘制其他内容时尝试更新 TextBox。
实际上,您正在打断队列,这可能会产生许多无法预料的后果。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
}
}
一旦您了解了 InvokeRequired,您可能希望考虑使用扩展方法来包装这些调用。这在 Stack Overflow 问题Clean Up Code Littered with Invoke Required中得到了很好的解决。
还有一篇关于历史上发生的事情的进一步记录,可能会引起人们的兴趣。
Windows 窗体中的控件或窗口对象只是由句柄(有时称为 HWND)标识的 Win32 窗口的包装。您对控件执行的大多数操作最终都会导致使用此句柄的 Win32 API 调用。句柄由创建它的线程(通常是主线程)拥有,不应由另一个线程操作。如果由于某种原因您需要对来自另一个线程的控件执行某些操作,您可以使用Invoke
请求主线程代表您执行此操作。
例如,如果您想从工作线程更改标签的文本,您可以执行以下操作:
theLabel.Invoke(new Action(() => theLabel.Text = "hello world from worker thread!"));
如果要修改控件,则必须在创建控件的线程中完成。此Invoke
方法允许您在关联线程(拥有控件的底层窗口句柄的线程)中执行方法。
在下面的示例中,thread1 引发异常,因为 SetText1 正在尝试从另一个线程修改 textBox1.Text。但是在thread2中,SetText2中的Action是在创建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"; });
实际上,这意味着保证在主线程上调用委托。这很重要,因为对于 Windows 控件,如果您不在主线程上更新它们的属性,那么您要么看不到更改,要么控件引发异常。
模式是:
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()
在主线程/创建的线程上调用委托参数。
我可以说 Thumb 规则不会访问您的表单控件,除非来自主线程。
以下几行可能对使用 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
if 返回 true 当前代码正在工作线程上运行,所以调用 this.Invoke(d, new object[] { text });
否则直接更新UI控件(这里保证你在主线程上运行代码。)
这意味着委托将在 UI 线程上运行,即使您从后台工作线程或线程池线程调用该方法也是如此。UI 元素具有线程亲和性——它们只喜欢直接与一个线程对话:UI 线程。UI 线程被定义为创建控件实例的线程,因此与窗口句柄相关联。但所有这些都是一个实现细节。
关键点是:您将从工作线程调用此方法,以便您可以访问 UI(更改标签中的值等) - 因为您不允许从 UI 线程以外的任何其他线程执行此操作。
代表本质上是内联Action
的或Func<T>
。您可以在您正在运行的方法范围之外或使用lambda
表达式(=>
)声明委托;因为您在一个方法中运行委托,所以您在为当前窗口/应用程序运行的线程上运行它,该线程是粗体。
拉姆达示例
int AddFiveToNumber(int number)
{
var d = (int i => i + 5);
d.Invoke(number);
}
这意味着您传递的委托在创建 Control 对象的线程(即 UI 线程)上执行。
当您的应用程序是多线程的并且您想从 UI 线程以外的线程执行一些 UI 操作时,您需要调用此方法,因为如果您只是尝试从不同的线程调用 Control 上的方法,您将得到一个System.InvalidOperationException。