【问题标题】:Should events raised by controls be on the UI thread?控件引发的事件是否应该在 UI 线程上?
【发布时间】:2012-05-05 01:25:08
【问题描述】:

我知道,当您是调用者、调用方法或以其他方式操作控件时,您应该调用 UI 线程(或无论如何拥有该控件的线程)来执行此操作。

但是,当控件通过其事件之一被回调时,是否可以安全地假设您在正确的线程上被调用?

根据我对常用控件的经验,这总是正确的,但也许这只是因为大多数事件是用户交互的结果,因此 Windows 消息由 UI 线程上的主消息循环处理。

最近我遇到了一个我自己的自定义控件的问题,它出于响应用户交互以外的原因调用事件,有时在后台线程上这样做。在一种情况下,该事件的事件处理程序试图操纵另一个控件,该控件产生了非法的跨线程调用异常。

我可以通过检查我的事件处理程序中是否需要调用来解决问题,但我很想知道这里实际上是谁“有错”。

我在任何地方都找不到任何说明有关控件事件的任何“规则”甚至最佳实践的文档。有人知道吗?或者,在您看来,是否应该由控件负责在正确的线程上调用订阅者,还是由订阅者负责检查?

编辑:似乎没有人听说过任何记录在案的约定,但普遍认为在控件所属线程上的Control-派生类上调用公共事件是个好主意,以避免消费者感到惊讶。

【问题讨论】:

    标签: .net winforms multithreading events


    【解决方案1】:

    我知道当你是调用者时,调用方法或其他方式 操作一个控件,你应该调用 UI 线程(或线程 无论如何,拥有该控制权)这样做。

    嗯...值得商榷。当然,您不应该尝试从托管它的线程以外的线程访问任何 UI 元素。然而,使用像Invoke 这样的编组技术并不总是更新 UI 元素的最佳机制,尤其是当您只想使用来自工作线程的进度信息更新 UI 时。别弄错我的意思。有时间和地点使用封送操作非常有意义,但很多时候让 UI 线程轮询它需要通过 System.Windows.Forms.Timer 更新自身的数据,这使得解决方案通常更简单、更高效、更简单优雅(如果不是更优雅)。

    但是,当被控件通过其中一个事件回调时, 假设您在正确的线程上被调用是否安全?

    不一定。我的意思是通常情况下,尤其是Control 实例。但请记住,您也可以在表单上删除 Component 实例。其中许多在其他线程上引发事件。将BackgroundWorker.DoWorkSerialPort.DataReceived 视为令人​​满意的反例。

    我可以通过检查调用是否正确来解决问题 在我的事件处理程序中是必需的,但我很想知道谁是 实际上在这里“有过错”。

    我不得不说是你的错。如果您的控件确实是 Control 的子类,那么我会真的努力确保所有事件都在 UI 线程上引发。如果您不这样做,那么它肯定会让其他(正确或错误)假设他们在 UI 线程上的开发人员感到困惑。如果您的“控件”仅继承 Component,那么您可能没问题,但请确保记录组件的行为。

    【讨论】:

    • 我的控件确实继承自 Control。正如您所说,Component 派生类经常调用其​​他线程上的事件,但这些组件没有任何自己的“UI”。我过度服务的模式只是来自 UI 控件的事件似乎总是在控件拥有线程上。大多数时候,我可以理解为什么会这样,但我想知道这实际上是应该遵循的某种约定,还是由于消息处理的工作方式而造成的巧合。不过,我想我会尝试在正确的线程上回调,如果只是因为开发人员可能会这样。
    • @Ashley:这是个好问题。老实说,我认为我也没有看到它记录在案,但我也从未见过Control 也不会在 UI 线程上引发其事件。我会按照您的直觉将这些事件编组到 UI 线程上,就像您已经建议的那样。如果你不这样做,那就太令人困惑了。
    【解决方案2】:

    避免在这里假设黑魔法在起作用,您描述的是理智的结果。如果您从工作线程引发事件,那么事件处理程序当然也会在该线程上运行。如果您在该处理程序中设置 UI 控件的属性,则会提醒您这样做是不合法的。

    完全相同的机制在控件引发的事件中起作用。它们根据 Windows 发送的通知消息进行操作,因此这发生在泵送消息循环的线程上。 Winforms 或 WPF 应用程序中的主线程始终是您的主线程,除非您正在做一些不明智的事情,例如在工作线程上创建窗口。因此,这些事件会在“正确”线程上引发,并且更新控件的属性不是问题。

    事件不会从一个线程跳到另一个线程,除非您明确编写代码来执行此操作。 Begin/Invoke(),你已经知道了。

    并注意 ab/使用 InvokeRequired,它是一种反模式。事先不知道特定代码运行在哪个线程上是不健康的。假设您在工作人员上执行长时间运行的 dbase 查询以避免冻结 UI。因此,当它的结果可用时,您知道您将不得不调用以显示结果。使用 InvokeRequired 没有任何意义,只是因为它能够告诉您确实有问题。一定要在返回 false 时抛出异常。

    【讨论】:

    • '并注意 ab/使用 InvokeRequired,这是一种反模式'谢谢!我永远无法理解那个调用,并怀疑一个函数有两个部分的“常见用法”,“如果 InvokeRequired threadBit else GUIbit”只是“关闭”。它只是给人的印象是开发人员不知道他们的代码是如何工作的。
    • 我理解 why 来自控件的事件通常在它们自己的线程上,由于它们的消息处理 - 我在问作为自定义控件的作者,我是否应该尝试确保我的事件总是在我拥有的线程上调用,或者这是否不是我的责任,而是那些订阅我的控件的人的责任来进行自己的检查。我找不到任何记录在案的约定,但我开始认为最好让控件确保它使用正确的线程,只是为了开发人员可能会认为它是这样的(就像我一样)。
    • 如果您从 Control 派生类发布自己的事件,那么如果该事件在另一个线程上引发,您将给使用您的控件的任何人一个很大的惊喜。您要么必须仔细记录,要么您应该自己处理编组。这当然很容易做到,你有需要调用 BeginInvoke 的 Control 实例,它是 this。但只有在事件不经常引发的情况下才这样做,编组非常昂贵。
    【解决方案3】:

    在我看来,如果我是您的控件的消费者,并且使用它开始引发线程异常,我可能会有点恼火:) 我想我会加入检查正在引发的事件的阵营,不取决于订阅者。

    【讨论】:

      猜你喜欢
      • 2021-12-09
      • 1970-01-01
      • 1970-01-01
      • 2013-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-14
      • 2015-04-24
      相关资源
      最近更新 更多