调用(委托)

108

有人能否请解释一下这篇链接上的语句?

Invoke(Delegate):

在拥有控件底层窗口句柄的线程上执行指定的委托。

有人能解释一下这是什么意思(特别是粗体部分)我没有弄清楚。


4
这个问题的答案与Control.InvokeRequired属性有关-请参阅http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invokerequired%28v=vs.90%29.aspx - dash
9个回答

146
这个问题的答案在于C#控件是如何工作的。
Windows窗体中的控件与特定线程绑定,不是线程安全的。因此,如果您从不同的线程调用控件的方法,必须使用其中一个控件的invoke方法将调用转发到正确的线程。如果您不知道哪个线程拥有控件,则可以使用此属性确定是否必须调用invoke方法,这可能很有用。
Control.InvokeRequired
实际上,Invoke所做的是确保您调用的代码发生在控件所"驻留的"线程上,有效地防止交叉线程异常。
从历史角度来看,在 .Net 1.1 中,实际上是允许这样做的。它意味着你可以尝试从任何后台线程执行代码到“GUI”线程,并且这通常会起作用。有时,这会导致应用程序退出,因为你实际上正在打断 GUI 线程,而它正在做其他事情。这就是“跨线程异常” - 想象一下在 GUI 正在绘制其他内容时尝试更新 TextBox。

  • 哪个操作优先?
  • 两者是否可能同时发生?
  • 所有 GUI 需要运行的其他命令会发生什么?

实际上,你正在中断一个队列,这可能会有很多不可预见的后果。Invoke 实际上是将你想要做的事情以“礼貌”的方式放入该队列中,从 .Net 2.0 开始通过抛出 InvalidOperationException 强制执行此规则。

为了理解幕后正在发生的事情以及所说的“GUI 线程”,了解消息泵或消息循环是有用的。

这个问题在 "什么是消息泵" 中已经得到了回答,阅读此文可更好地理解与控件交互时所涉及的实际机制。

其他有用的阅读材料包括:

Begin Invoke 是怎么回事

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的问题清理与Invoke Required混杂的代码中得到了很好的解答。
此外,还有一个历史上发生的事情的详细描述,可能会引起您的兴趣。

79

在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
1
@KyleDelaney,一个对象并不是“在”一个线程中,当前的线程也不一定是创建该对象的线程。 - Thomas Levesque

34

如果您想修改控件,必须在创建控件的线程中进行。该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"));
}

我真的很喜欢这种方法,它隐藏了委托的本质,但无论如何都是一个不错的快捷方式。 - shytikov
感谢您提供了一个简洁的示例,正好符合我的需求。 - Skyfish

9
Invoke((MethodInvoker)delegate{ textBox1.Text = "Test"; });

使用System.Action的建议仅适用于3.5+框架,对于旧版本,此方法完美适用。 - Suicide Platypus

3

实际上,这意味着代理将保证在主线程上被调用。这很重要,因为在窗口控件的情况下,如果您不在主线程上更新它们的属性,则要么看不到更改,要么控件会引发异常。

模式如下:

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

3

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 控件 (在这里,您可以确保您正在主线程上运行代码)。


感谢您提供如何在控件上使用InvokeRequired的示例。 - Skyfish

2
这意味着即使您从后台工作器或线程池线程调用该方法,委托也将在UI线程上运行。 UI元素具有“线程亲和性”,它们只想直接与一个线程通信:UI线程。 UI线程被定义为创建控件实例的线程,因此与窗口句柄相关联。但所有这些都是实现细节。 关键点是:您应该从工作线程调用此方法,以便访问UI(更改标签中的值等)-因为您不允许从UI线程以外的任何其他线程执行此操作。

0
委托本质上是内联的 Action 或 Func。你可以在运行方法之外声明一个委托,或使用 lambda 表达式 (=>);因为你在方法内运行委托,所以它运行在当前窗口/应用程序所运行的线程上,这就是加粗部分。
Lambda 示例
int AddFiveToNumber(int number)
{
  var d = (int i => i + 5);
  d.Invoke(number);
}

0

这意味着你传递的委托将在创建 Control 对象的线程上执行(即 UI 线程)。

当你的应用程序是多线程的,并且你想从一个非 UI 线程执行一些 UI 操作时,你需要调用此方法,因为如果你尝试从不同的线程调用 Control 上的方法,你会得到一个 System.InvalidOperationException。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接