如何将事件从使用计时器的对象引发回UI线程

时间:2012-03-01 22:50:12

标签: c# multithreading

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

有没有办法从我的对象的用户那里隐藏这些额外的努力?

为了讨论的目的,我将包括一个简单的例子:

想象一下,我有一个包含1个richtextbox的表单:

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

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

感谢您提供的任何意见。

3 个答案:

答案 0 :(得分:4)

评论后添加:

你需要获取一些可以利用的潜在细节,以便能够实现这一目标。

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

这是一种相当黑客的方式来实现你想要的东西,但它可以完成契约,因为你的民意调查监听器的构造发生在消费者的线程中(它有一个Windows消息泵来为表格的调度提供动力) WPF定时器),类的其余操作可以从任何线程发生,因为表单Timer的tick会从原始线程中心跳。 正如其他评论和回答所指出的那样,最好重新评估并修复投票操作与消费者之间的操作关系

这是类的更新版本,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.");
    }
}

答案 1 :(得分:0)

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

答案 2 :(得分:0)

This SO question提示此评论:

  

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

System.Times.Timer doc说的是SynchronizingObject:

  

获取或设置用于封送事件处理程序调用的对象   在间隔过去时发出。

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