如何使事件回调进入Win窗体线程安全?

时间:2020-03-05 18:38:39  来源:igfitidea点击:

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

问题在于,调用回调时,我们不能假定可以在窗体上进行更新控件,因为有时,如果在与运行窗体的线程不同的线程上调用事件回调,则这些控件将引发期望。

解决方案

回答

这里是要点:

  • 我们不能从不同于在其上创建线程(窗体线程)的线程进行UI控件调用。
  • 委托调用(即事件挂钩)在与触发事件的对象相同的线程上触发。

因此,如果我们有一个单独的"引擎"线程来执行某些工作,并且有一些UI监视状态变化(这些状态变化可以反映在UI中)(例如进度条或者其他),那么我们就会遇到问题。引擎触发的是一个对象更改事件,该事件已被Form挂起。但是,向引擎注册的Form的回调委托是在引擎的线程而不是Form的线程上调用的。因此,我们无法从该回调中更新任何控件。 h!

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,因为我们现在位于正确的线程上,每个人都很高兴。

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

回答

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

回答

为了稍微简化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";
}

回答

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

void SomethingHappened(object sender, EventArgs ea)
{
   MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
   InvokeRequired ? Invoke( del ) : del(); 
}

回答

我对这个话题有点晚了,但是我们可能想看一下基于事件的异步模式。正确实施后,它可以确保始终从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; }
        }
    }
}