简体   繁体   English

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

[英]How do I make event callbacks into my win forms thread safe?

When you subscribe to an event on an object from within a form, you are essentially handing over control of your callback method to the event source.当您从表单中订阅 object 上的事件时,您实际上是将回调方法的控制权交给了事件源。 You have no idea whether that event source will choose to trigger the event on a different thread.您不知道该事件源是否会选择在不同的线程上触发事件。

The problem is that when the callback is invoked, you cannot assume that you can make update controls on your form because sometimes those controls will throw an exception if the event callback was called on a thread different than the thread the form was run on.问题是当回调被调用时,你不能假设你可以在你的表单上创建更新控件,因为有时如果事件回调是在与表单运行的线程不同的线程上调用的,这些控件会抛出异常。

To simplify Simon's code a bit, you could use the built in generic Action delegate.为了稍微简化 Simon 的代码,您可以使用内置的通用 Action 委托。 It saves peppering your code with a bunch of delegate types you don't really need.它可以避免在你的代码中添加一堆你并不真正需要的委托类型。 Also, in .NET 3.5 they added a params parameter to the Invoke method so you don't have to define a temporary array.此外,在 .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";
}

Here are the salient points:以下是要点:

  1. You can't make UI control calls from a different thread than the one they were created on (the form's thread).您不能从与创建它们的线程(表单的线程)不同的线程进行 UI 控件调用。
  2. Delegate invocations (ie, event hooks) are triggered on the same thread as the object that is firing the event.委托调用(即事件挂钩)在与触发事件的 object 相同的线程上触发。

So, if you have a separate "engine" thread doing some work and have some UI watching for state changes which can be reflected in the UI (such as a progress bar or whatever), you have a problem.所以,如果你有一个单独的“引擎”线程做一些工作,并且有一些 UI 观察 state 的变化,这些变化可以反映在 UI 中(例如进度条或其他),你就有问题了。 The engine fire's an object changed event which has been hooked by the Form.引擎着火是一个 object 更改事件,该事件已被表单挂钩。 But the callback delegate that the Form registered with the engine gets called on the engine's thread… not on the Form's thread.但是在引擎的线程上调用了 Form 向引擎注册的回调委托……而不是在 Form 的线程上。 And so you can't update any controls from that callback.因此,您无法从该回调中更新任何控件。 Doh!嗬!

BeginInvoke comes to the rescue. BeginInvoke来救援。 Just use this simple coding model in all your callback methods and you can be sure that things are going to be okay:只需在所有回调方法中使用这个简单的编码 model,您就可以确定一切都会好起来的:

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";
}

It's quite simple really.这真的很简单。

  1. Use InvokeRequired to find out if this callback happened on the correct thread.使用InvokeRequired确定此回调是否发生在正确的线程上。
  2. If not, then reinvoke the callback on the correct thread with the same parameters.如果不是,则使用相同的参数在正确的线程上重新调用回调。 You can reinvoke a method by using the Invoke (blocking) or BeginInvoke (non-blocking) methods.您可以使用Invoke (阻塞)或BeginInvoke (非阻塞)方法重新调用方法。
  3. The next time the function is called, InvokeRequired returns false because we are now on the correct thread and everybody is happy.下次调用 function 时, InvokeRequired返回 false,因为我们现在在正确的线程上并且每个人都很高兴。

This is a very compact way of addressing this problem and making your Forms safe from multi-threaded event callbacks.这是解决此问题的一种非常紧凑的方法,并使您的 Forms 免受多线程事件回调的影响。

I use anonymous methods a lot in this scenario:在这种情况下,我经常使用匿名方法:

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

I'm a bit late to this topic, but you might want to take a look at the Event-Based Asynchronous Pattern .我对这个主题有点晚了,但你可能想看看基于事件的异步模式 When implemented properly, it guarantees that events are always raised from the UI thread.如果实施得当,它保证总是从 UI 线程引发事件。

Here's a brief example that only allows one concurrent invocation;这是一个只允许一个并发调用的简短示例; supporting multiple invocations/events requires a little bit more plumbing.支持多个调用/事件需要更多的管道。

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

As the lazy programmer , I have a very lazy method of doing this.作为lazy programmer ,我有一个非常懒惰的方法。

What I do is simply this.我所做的只是这个。

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

You could inline the DoInvoke inside your function or hide it within separate function to do the dirty work for you.您可以将 DoInvoke 内联到 function 中,或者将其隐藏在单独的 function 中,为您完成脏活。

Just keep in mind you can pass functions directly into the DoInvoke method.请记住,您可以将函数直接传递给 DoInvoke 方法。

private void directPass() {
    DoInvoke(this.directInvoke);
}
private void directInvoke() {
    textLabel.Text = "Directly passed.";
}

In many simple cases, you can use the MethodInvoker delegate and avoid the need to create your own delegate type.在许多简单的情况下,您可以使用 MethodInvoker 委托,而无需创建自己的委托类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM