【问题标题】:How to raise an event from an object that uses a timer back to the UI thread如何从使用计时器的对象中将事件引发回 UI 线程
【发布时间】:2012-03-02 08:35:57
【问题描述】:

我有一个对象,它使用计时器偶尔轮询资源,然后在轮询发现值得注意的东西时引发事件。我查看了其他几个示例,但似乎无法找到一种方法将事件编组回 UI 线程,而无需在 UI 线程上的事件处理程序上添加额外代码。所以我的问题是:

有什么方法可以对我的对象的用户隐藏这种额外的努力吗?

为了便于讨论,我将举一个简单的例子:

假设我有一个带有 1 个富文本框的表单:

private void Form1_Load(object sender, EventArgs e)
{
    var listener = new PollingListener();
    listener.Polled += new EventHandler<EventArgs>(listener_Polled);
}

void listener_Polled(object sender, EventArgs e)
{
    richTextBox1.Text += "Polled " + DateTime.Now.Second.ToString();
}

我也有这个对象:

public class PollingListener
{
    System.Timers.Timer timer = new System.Timers.Timer(1000);
    public event EventHandler<EventArgs> Polled;
    public PollingListener()
    {
        timer.Elapsed +=new System.Timers.ElapsedEventHandler(PollNow);
        timer.Start();
    }

    void PollNow(object sender, EventArgs e)
    {
        var temp = Polled;
        if (temp != null) Polled(this, new EventArgs());

    }
}

如果我运行它,正如预期的那样,它会产生异常

"跨线程操作无效:已访问控件'richTextBox1' 来自创建它的线程以外的线程”

这对我来说很有意义,我可以像这样以不同的方式包装事件处理程序方法:

void listener_Polled(object sender, EventArgs e)
{
    this.BeginInvoke(new Action(() => { UpdateText() }));
}
void UpdateText()
{
    richTextBox1.Text += "Polled " + DateTime.Now.Second.ToString();
}

但是现在我的对象的用户必须为我控制的计时器事件引发的任何事件执行此操作。那么,有什么我可以添加到我的 PollingListener 类中的东西,它不会改变它的方法的签名以传递额外的引用,从而允许我的对象的用户忘记后台到 UI 线程的封送事件?

感谢您的任何意见。

【问题讨论】:

  • 只需使用 WinForms 或 WPF 计时器。删掉大部分代码。
  • 如果从那些定时器调用他的轮询操作可能会阻塞任何显着的时间,那么这些定时器可能会阻塞 UI 线程。
  • 我个人认为隐藏你在后台线程上的事实并不是一个好主意。这是您的用户应该注意的事情(我假设您所说的用户是指其他程序员)。用户可能希望事件在默认 UI 线程之外的线程中触发,或者根本不关心它触发的线程 - 这是他们的调用,而不是您的调用(作为实用程序类的作者)。
  • 虽然这里有几个优点,并且诚然,这里大量使用计时器导致与我更(不太明显)的基本问题产生分歧,但我一直在寻找构建对象的方法记住哪个线程创建了它,如果它是某种 UI 线程,它会适当地编组。考虑到该设计的脆弱性,尽管我想不出任何好主意。我假设有几个现有的对象在内部使用线程,引发事件,并为我将它们冒泡到 UI 线程 wo/Form refs,但显然不是。

标签: c# multithreading


【解决方案1】:

评论后添加:

您需要收集一些可以利用的潜在细节来实现该目标。

想到的一件事是在构建时创建自己的 Forms/WPF 计时器,然后使用它和一些同步来隐藏跨线程协调的细节。我们可以从您的示例中推断出您的轮询器的构建应该始终在您的消费者线程的上下文中进行。

这是完成您想要的事情的一种相当 hack-ish 的方式,但它可以完成任务,因为您的 poll-listener 的构造发生在消费者的线程中(它有一个 windows 消息泵来推动 Forms/ 的调度) WPF 计时器),并且该类的其余操作可以从任何线程发生,因为表单计时器的滴答声将来自原始线程的心跳。 正如其他 cmets 和答案所指出的,最好重新评估并修复您的投票操作与消费者之间的操作关系

这是该类的更新版本 PollingListener2,它使用 ManualResetEvent 和隐藏的 System.Windows.Forms.Timer 在线程间传递轮询通知。为简洁起见,省略了清理代码。建议在此类的生产版本中使用 IDisposable 进行显式清理。

ManualResetEvent @ MSDN

public class PollingListener2
{
    System.Timers.Timer timer = new System.Timers.Timer(1000);
    public event EventHandler<EventArgs> Polled;

    System.Windows.Forms.Timer formsTimer;
    public System.Threading.ManualResetEvent pollNotice;

    public PollingListener2()
    {
        pollNotice = new System.Threading.ManualResetEvent(false);
        formsTimer = new System.Windows.Forms.Timer();
        formsTimer.Interval = 100;
        formsTimer.Tick += new EventHandler(formsTimer_Tick);
        formsTimer.Start();
        timer.Elapsed += new System.Timers.ElapsedEventHandler(PollNow);
        timer.Start();
    }

    void formsTimer_Tick(object sender, EventArgs e)
    {
        if (pollNotice.WaitOne(0))
        {
            pollNotice.Reset();
            var temp = Polled;
            if (temp != null)
            {
                Polled(this, new EventArgs());
            }
        }
    }

    void PollNow(object sender, EventArgs e)
    {
        pollNotice.Set();
    }
}

这在遥远的 Win32 过去有一些先例,有些人会使用隐藏窗口等来保持一只脚在另一个线程中,而无需消费者对其代码进行任何重大更改(有时不需要更改)。


原文:

您可以在您的帮助器类上添加一个 ControlForm 类型的成员变量,并将其用作在您的事件调度中调用 BeginInvoke() / Invoke() 的范围。

这是您的示例类的副本,已修改为以这种方式运行。

public class PollingListener
{
    System.Timers.Timer timer = new System.Timers.Timer(1000);
    public event EventHandler<EventArgs> Polled;

    public PollingListener(System.Windows.Forms.Control consumer)
    {
        timer.Elapsed += new System.Timers.ElapsedEventHandler(PollNow);
        timer.Start();
        consumerContext = consumer;
    }

    System.Windows.Forms.Control consumerContext;

    void PollNow(object sender, EventArgs e)
    {
        var temp = Polled;
        if ((temp != null) && (null != consumerContext))
        {
            consumerContext.BeginInvoke(new Action(() =>
                {
                    Polled(this, new EventArgs());
                }));
        }
    }
}

这里有一个示例,展示了这一点。在调试模式下运行它并查看您的输出以验证它是否按预期工作。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        listener = new PollingListener(this);
    }

    PollingListener listener;

    private void Form1_Load(object sender, EventArgs e)
    {
        listener.Polled += new EventHandler<EventArgs>(listener_Poll);
    }

    void listener_Poll(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("ding.");
    }
}

【讨论】:

  • 我以前用过它,而且肯定可以,但我正在寻找一种不需要明确地为我的对象提供对我的表单/控件的引用的解决方案。我试图通过提及不更改我的方法签名来适应这一点来表达这一点,但我从未提及属性。
【解决方案2】:

如果 PollNow 中的处理工作相当小,那么您不需要在单独的线程上执行它。如果 WinForms 使用 Timer,则在 WPF 中使用 DispatchTimer,然后在与 UI 相同的线程上执行测试,不存在跨线程问题。

【讨论】:

    【解决方案3】:

    This SO question 提示这条评论:

    我认为这段摘录很有启发性:“不像 System.Windows.Forms.Timer,System.Timers.Timer 类将通过 默认情况下,在获得的工作线程上调用您的计时器事件处理程序 来自公共语言运行时 (CLR) 线程池。 [...] 这 System.Timers.Timer 类提供了一种简单的方法来处理这个 困境——它公开了一个公共的 SynchronizingObject 属性。设置这个 属性到 Windows 窗体的实例(或 Windows 上的控件 Form) 将确保 Elapsed 事件处理程序中的代码在 实例化 SynchronizingObject 的同一线程。"

    System.Times.Timer 文档提到了 SynchronizingObject:

    获取或设置用于编组事件处理程序调用的对象 间隔已过时发出。

    这两者都暗示如果您将在 UI 线程上创建的控件作为同步对象传递,那么计时器将有效地将计时器事件调用编组到 UI 线程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-12
      相关资源
      最近更新 更多