【问题标题】:Need explanation on event handling and delegates需要解释事件处理和委托
【发布时间】:2011-02-23 06:40:40
【问题描述】:

我有一些我写的代码,它可以满足我的需求。但是,我不太确定它究竟是如何工作的。我最麻烦的部分是最后一部分。我有一个 textBox1.Text = "test" 不起作用。我收到一个运行时错误,它是从不同的线程调用的。当我放置 textBox1.Invoke(等)时,它按预期工作。为什么?

如您所见,我知道这足以造成危险,我真的想了解这里发生了什么,而不是盲目地从网络上的网站复制和粘贴。

我在名为 SerialCommunicator 的类中有以下内容:

public SerialCommunicator(SerialPort sp)
{
    this.sp = sp;
    sp.ReceivedBytesThreshold = packetSize;
    sp.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
    sp.Open();
}    
public void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    Thread.Sleep(50);
    SerialPort s = (SerialPort)sender;
    byte[] buffer = new byte[128];
    s.Read(buffer, 0, s.BytesToRead);
}

然后,在我的 Form1.cs 中,我有一个按钮,按下时会执行以下操作:

private void btnPortOK_Click(object sender, EventArgs e)
{
    string comPort = cboComPorts.SelectedItem.ToString();
    SerialPort sp = new SerialPort(comPort, 9600, Parity.None, 8, StopBits.One);
    sp.DataReceived += new SerialDataReceivedEventHandler(DataHasBeenReceived);
    comm = new SerialCommunicator(sp);
}
public void DataHasBeenReceived(object sender, EventArgs args)
{
    textBox1.Invoke(new EventHandler(delegate { textBox1.Text += "test"; }));
}

【问题讨论】:

    标签: c# .net multithreading delegates invoke


    【解决方案1】:

    这是线程关联。 UI 控件不喜欢被创建它们的线程之外的任何东西触及,但DataReceived 线程发生在不同的线程中。添加对Control.Invoke 的调用会将一项工作推回UI 线程,因此Text 更新可以成功。

    【讨论】:

      【解决方案2】:

      我不是这方面的专家(可能会有比这更好的答案)。但据我了解,GUI 线程“拥有”您的表单。因此,当您尝试从不同的线程更新它时,您正在跨越流。

      Invoke 是一种要求 GUI 线程运行方法的方法。它运行的方法是你的textBox1.Text += "test";

      这个想法是通过调用一个委托,它会要求 GUI 线程进行更改,而不仅仅是自己更改值。这允许以线程安全的方式进行更改。

      这是 Jon Skeet 关于这个问题的一篇好文章: http://www.yoda.arachsys.com/csharp/threads/winforms.shtml

      【讨论】:

      • +1 链接。我会接受这个答案或任何解释传递给 Invoke 的部分的答案(在我看来,根据 MSDN,invoke 需要一个委托,但在这里它被传递了一个 EventHandler 类型,该类型具有一个委托作为其参数)。如您所见,我想在串口接收到数据时“做点什么”。可以将 textBox1.Invoke(etc...) 写成“更简单”吗?
      • Control.Invoke 接受委托。 EventHandler 是一种委托(此处的文档:msdn.microsoft.com/en-us/library/db0etb8x.aspx),因此如果您愿意,它可以采用。或者你可以在没有 EventHandler 包装器的情况下传入 lambda 表达式(作为委托)。我能想到的编写代码的最压缩方式是这样的:textBox1.Invoke((Action)(()=> textBox1.Text += "test"));
      • Jon Skeet 展示了如果您想在此处避免强制转换,您可以如何使用扩展方法:stackoverflow.com/questions/411579/…
      【解决方案3】:

      事件从它们发生的线程调用。 (除非另有说明)。

      这样想:
      当您激活事件时,它实际上被称为函数 EventName()。所以调用一个事件意味着实际上去所有注册到该事件的方法并执行它们。
      但是,这是在同一个线程中以串行方式完成的。

      因此,如果事件发生在不是您的 UI 线程的线程中,您将收到该错误。

      【讨论】:

        【解决方案4】:

        问题在于 GUI 组件只接受来自 GUI 线程的修改。因此,当其他线程想要修改 GUI 时,他们必须使用诸如 control.Invoke(...) 之类的措施将其修改代码排队,这将使委托在 GUI 事件队列中尽快处理,从而得到正确的线程。

        您遇到的是内置检查之一被触发而不是调用线程确实是正确线程的控件。这是一种使调试更容易的安全措施(如果它们不存在,您将不得不调试微妙的线程问题......)

        【讨论】:

          【解决方案5】:

          textBox1.Text = "test" 不起作用,因为您是从另一个线程(即DataHasBeenReceived 事件)调用它,然后是拥有文本框的线程。这通常是您的应用程序在其中运行并创建您的 GUI 界面(以及您的文本框)的线程。 Invoke 有效,因为该方法切换到 GUI 线程,设置文本,然后切换回 DataHasBeenReceived 事件的线程。

          在 Net 1.0 和 1.1 中,您可以使用来自另一个线程的 GUI 控件,然后是拥有它们的那个,但是当线程同时开始访问控件时,这会导致很多问题。所以,自从 net 2.0 以来,微软改变了这一点。

          如果您想知道是否必须使用调用(即是否可以从 GUI 线程或另一个线程调用方法),您可以使用属性 InvokeRequired 结合 if else。调用调用比直接操作控件稍微贵一些。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-05-16
            • 1970-01-01
            • 2013-08-11
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多