调用(委托)


Answers:


129

这个问题的答案在于C#控件的工作方式

Windows窗体中的控件绑定到特定线程,并且不是线程安全的。因此,如果要从其他线程调用控件的方法,则必须使用控件的invoke方法之一将对适当线程的调用编组。此属性可用于确定是否必须调用invoke方法,如果您不知道哪个线程拥有控件,该属性将很有用。

Control.InvokeRequired

实际上,Invoke所做的是确保您正在调用的代码出现在控件“存在”的线程上,从而有效地防止了跨线程异常。

从历史的角度来看,在.Net 1.1中,实际上是允许这样做的。它的意思是您可以尝试从任何后台线程在“ GUI”线程上执行代码,这将大体上可行。有时,这只会导致您的应用退出,因为您在执行其他操作时实际上正在中断GUI线程。这是跨线程异常 -想象一下在GUI绘制其他内容时尝试更新TextBox。

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

实际上,您正在中断队列,这可能会带来很多无法预料的后果。调用实际上是将您想做的事情放入该队列的“礼貌”方式,并且此规则是从.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后,您不妨考虑使用扩展方法来打包这些调用。堆栈溢出问题清理因请求调用而乱扔的代码可以很好地解决此问题。

对于历史上可能发生的事情,还有进一步的记载。


67

Windows窗体中的控件或窗口对象只是由句柄(有时称为HWND)标识的Win32窗口的包装。使用控件执行的大多数操作最终都会导致使用此句柄的Win32 API调用。该句柄由创建它的线程(通常是主线程)拥有,并且不应被另一个线程操纵。如果出于某种原因您需要对另一个线程中的控件执行某些操作,则可以使用Invoke要求主线程代表您执行此操作。

例如,如果您想从工作线程中更改标签的文本,则可以执行以下操作:

theLabel.Invoke(new Action(() => theLabel.Text = "hello world from worker thread!"));

您能解释为什么有人会这样做吗?在当前线程中肯定是指this.Invoke(() => this.Enabled = true);什么this,对吗?
凯尔·德莱尼

1
@KyleDelaney,对象不是“在”线程中,并且当前线程不一定是创建该对象的线程。
托马斯·列维斯克

24

如果要修改控件,则必须在创建控件的线程中完成。此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"));
}

我真的很喜欢这种方法,它隐藏了代表的本质,但是无论如何这都是不错的捷径。
shytikov 2015年

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

人们建议在其他响应上使用System.Action仅在框架3.5+上有效,对于较旧的版本,这非常有效
Suicide Platypus

2

实际上,这意味着可以确保在主线程上调用委托。这一点很重要,因为对于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)
}

2

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

在某些情况下,尽管您创建了一个Threadpool线程(即工作线程),但它将在主线程上运行。它不会创建新的线程,因为主线程可用于处理更多指令。因此,首先使用this.InvokeRequiredif返回true 来调查当前正在运行的线程是否是主线程,当前代码正在工作线程上运行,因此调用此方法。

否则,直接更新UI控件(此处确保在主线程上运行代码。)


1

这意味着即使您从后台工作程序或线程池线程调用该方法,委托也将在UI线程上运行。UI元素具有线程关联性 -它们仅喜欢直接与一个线程:UI线程对话。UI线程被定义为创建控件实例的线程,因此与窗口句柄相关联。但是,所有这些都是实现细节。

关键点是:您将通过辅助线程调用此方法,以便可以访问UI(以更改标签中的值,等等)-因为除UI线程外,不允许您从任何其他线程中进行此操作。


0

委托本质上是内联的 Action的或Func<T>。您可以在正在运行的方法范围内或使用lambdaexpression(=>)声明委托。因为您在一个方法中运行委托,所以您在当前窗口/应用程序正在运行的线程上运行该委托,该线程以粗体显示。

Lambda示例

int AddFiveToNumber(int number)
{
  var d = (int i => i + 5);
  d.Invoke(number);
}

0

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

当您的应用程序是多线程的并且您想要从UI线程以外的线程中执行一些UI操作时,您需要调用此方法,因为如果您只是尝试从其他线程中调用Control上的方法, System.InvalidOperationException。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.