如何使事件回调进入我的 win 表单线程安全?

Posted

技术标签:

【中文标题】如何使事件回调进入我的 win 表单线程安全?【英文标题】:How do I make event callbacks into my win forms thread safe? 【发布时间】:2010-09-05 14:12:26 【问题描述】:

当您从表单中订阅对象上的事件时,您实际上是将回调方法的控制权交给了事件源。您不知道该事件源是否会选择在不同的线程上触发事件。

问题是当回调被调用时,你不能假设你可以在你的表单上创建更新控件,因为有时如果事件回调是在与表单运行的线程不同的线程上调用的,这些控件会抛出异常开。

【问题讨论】:

【参考方案1】:

以下是重点:

    您不能从与创建它们的线程(表单线程)不同的线程调用 UI 控件。 委托调用(即事件挂钩)在与触发事件的对象相同的线程上触发。

所以,如果你有一个单独的“引擎”线程在做一些工作,并且有一些 UI 监视状态变化,这些变化可以反映在 UI 中(例如进度条或其他),那么你就有问题了。引擎火灾是一个对象更改事件,该事件已被表单挂钩。但是在引擎的线程上调用了 Form 向引擎注册的回调委托……而不是在 Form 的线程上。因此,您无法从该回调中更新任何控件。呵呵!

BeginInvoke 来救援。只需在所有回调方法中使用这个简单的编码模型,就可以确定一切都会好起来的:

private delegate void EventArgsDelegate(object sender, EventArgs ea);

void SomethingHappened(object sender, EventArgs ea)

   //
   // Make sure this callback is on the correct thread
   //
   if (this.InvokeRequired)
   
      this.Invoke(new EventArgsDelegate(SomethingHappened), new object[]  sender, ea );
      return;
   

   //
   // Do something with the event such as update a control
   //
   textBox1.Text = "Something happened";

其实很简单。

    使用 InvokeRequired 确定此回调是否发生在正确的线程上。 如果不是,则使用相同的参数在正确的线程上重新调用回调。您可以使用 Invoke(阻塞)或 BeginInvoke(非阻塞)方法重新调用方法。 下次调用该函数时,InvokeRequired 返回 false,因为我们现在在正确的线程上并且每个人都很高兴。

这是解决此问题并使您的表单免受多线程事件回调影响的一种非常紧凑的方法。

【讨论】:

我通常更喜欢 BeginInvoke 而不是 Invoke,但有一个警告:必须避免排队太多事件。我使用一个 updateRequired 变量,当 BeginInvoke 发生时设置为 1,并且只有在它为零时才执行 BeginInvoke(使用 Interlocked.Exchange)。显示处理程序有一个清除 updateRequired 的 while 循环,如果它不为零,则执行更新并循环。在某些情况下,会添加一个计时器以进一步限制更新频率(以避免代码将所有时间都花在更新进度读数而不是做实际工作),但这更复杂。 @Supercat... 事件限制是许多应用程序的重要主题,但它不应该成为 UI 层的一部分。应该创建一个单独的事件代理总线来以适当的时间间隔接收、排队、组合和重新发送事件。事件总线的任何订阅者都不应该知道正在发生事件限制。 我可以看到一个单独的“事件总线”来处理同步可能有用的地方,但在许多情况下,对于像进度指示器类这样的最终用户来说,如果该类只是暴露了一个 MinimumUpdateInterval 属性。【参考方案2】:

在许多简单的情况下,您可以使用 MethodInvoker 委托,而无需创建自己的委托类型。

【讨论】:

【参考方案3】:

为了稍微简化 Simon 的代码,您可以使用内置的通用 Action 委托。它可以避免在你的代码中添加一堆你并不真正需要的委托类型。此外,在 .NET 3.5 中,他们向 Invoke 方法添加了一个 params 参数,因此您不必定义临时数组。

void SomethingHappened(object sender, EventArgs ea)

   if (InvokeRequired)
   
      Invoke(new Action<object, EventArgs>(SomethingHappened), sender, ea);
      return;
   

   textBox1.Text = "Something happened";

【讨论】:

【参考方案4】:

在这种情况下我经常使用匿名方法:

void SomethingHappened(object sender, EventArgs ea)

   MethodInvoker del = delegate textBox1.Text = "Something happened"; ; 
   InvokeRequired ? Invoke( del ) : del(); 

【讨论】:

【参考方案5】:

我对这个话题有点晚了,但你可能想看看Event-Based Asynchronous Pattern。如果实施得当,它保证总是从 UI 线程引发事件。

这是一个只允许一个并发调用的简短示例;支持多个调用/事件需要更多的管道。

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1

    public class MainForm : Form
    
        private TypeWithAsync _type;

        [STAThread()]
        public static void Main()
        
            Application.EnableVisualStyles();
            Application.Run(new MainForm());
        

        public MainForm()
        
            _type = new TypeWithAsync();
            _type.DoSomethingCompleted += DoSomethingCompleted;

            var panel = new FlowLayoutPanel()  Dock = DockStyle.Fill ;

            var btn = new Button()  Text = "Synchronous" ;
            btn.Click += SyncClick;
            panel.Controls.Add(btn);

            btn = new Button  Text = "Asynchronous" ;
            btn.Click += AsyncClick;
            panel.Controls.Add(btn);

            Controls.Add(panel);
        

        private void SyncClick(object sender, EventArgs e)
        
            int value = _type.DoSomething();
            MessageBox.Show(string.Format("DoSomething() returned 0.", value));
        

        private void AsyncClick(object sender, EventArgs e)
        
            _type.DoSomethingAsync();
        

        private void DoSomethingCompleted(object sender, DoSomethingCompletedEventArgs e)
        
            MessageBox.Show(string.Format("DoSomethingAsync() returned 0.", e.Value));
        
    

    class TypeWithAsync
    
        private AsyncOperation _operation;

        // synchronous version of method
        public int DoSomething()
        
            Thread.Sleep(5000);
            return 27;
        

        // async version of method
        public void DoSomethingAsync()
        
            if (_operation != null)
            
                throw new InvalidOperationException("An async operation is already running.");
            

            _operation = AsyncOperationManager.CreateOperation(null);
            ThreadPool.QueueUserWorkItem(DoSomethingAsyncCore);
        

        // wrapper used by async method to call sync version of method, matches WaitCallback so it
        // can be queued by the thread pool
        private void DoSomethingAsyncCore(object state)
        
            int returnValue = DoSomething();
            var e = new DoSomethingCompletedEventArgs(returnValue);
            _operation.PostOperationCompleted(RaiseDoSomethingCompleted, e);
        

        // wrapper used so async method can raise the event; matches SendOrPostCallback
        private void RaiseDoSomethingCompleted(object args)
        
            OnDoSomethingCompleted((DoSomethingCompletedEventArgs)args);
        

        private void OnDoSomethingCompleted(DoSomethingCompletedEventArgs e)
        
            var handler = DoSomethingCompleted;

            if (handler != null)  handler(this, e); 
        

        public EventHandler<DoSomethingCompletedEventArgs> DoSomethingCompleted;
    

    public class DoSomethingCompletedEventArgs : EventArgs
    
        private int _value;

        public DoSomethingCompletedEventArgs(int value)
            : base()
        
            _value = value;
        

        public int Value
        
            get  return _value; 
        
    

【讨论】:

我认为说“它保证事件总是从 UI 线程引发”有点误导。说它确保事件处理程序在创建任务的同一个 SynchronizationContext / 线程上执行不是更准确吗? (可能不是 UI 线程/SynchronizationContext)【参考方案6】:

作为lazy programmer,我有一个非常懒惰的方法。

我所做的只是这个。

private void DoInvoke(MethodInvoker del) 
    if (InvokeRequired) 
        Invoke(del);
     else 
        del();
    

//example of how to call it
private void tUpdateLabel(ToolStripStatusLabel lbl, String val) 
    DoInvoke(delegate  lbl.Text = val; );

您可以将 DoInvoke 内联到您的函数中,或将其隐藏在单独的函数中,以便为您完成繁琐的工作。

请记住,您可以将函数直接传递给 DoInvoke 方法。

private void directPass() 
    DoInvoke(this.directInvoke);

private void directInvoke() 
    textLabel.Text = "Directly passed.";

【讨论】:

我完全支持惰性编程 :) 如果您使用的是 .NET 3.5 或更高版本,您可以使用 ActionAction&lt;object, EventArgs&gt; 以及 lambda 表达式:Doinvoke(() =&gt; textLabel.Text = "Something")

以上是关于如何使事件回调进入我的 win 表单线程安全?的主要内容,如果未能解决你的问题,请参考以下文章

如何使 Stack.Pop 线程安全

进入Qt事件循环后如何自动执行方法?

如何使字典 NSDictionary 成为阅读、插入的线程安全?

Windows XP线程的调度策略

如何在收到异步函数的回调之前使线程休眠?

如何使这个线程安全