如何将事件回调变为我的win表单线程安全?

时间:2008-08-08 17:32:42

标签: c# .net winforms multithreading events

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

问题是,当调用回调时,你不能假设你可以在表单上进行更新控制,因为如果在不同于运行表单的线程的线程上调用事件回调,那么这些控件会抛出异常上。

6 个答案:

答案 0 :(得分:31)

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

答案 1 :(得分:16)

以下是重点:

  1. 您不能从与其创建的线程不同的线程(表单的线程)进行UI控件调用。
  2. 委托调用(即事件挂钩)在与触发事件的对象相同的线程上触发。
  3. 所以,如果你有一个单独的“引擎”线程做一些工作并且有一些UI观察状态变化,这可以在UI中反映出来(例如进度条或其他),那么你就会遇到问题。引擎触发了一个对象已更改的事件,该事件已被表单挂钩。但是,引擎在引擎中注册的回调委托在引擎的线程上被调用...而不是在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";
    }
    

    真的很简单。

    1. 使用 InvokeRequired 查看此回调是否发生在正确的主题上。
    2. 如果没有,则使用相同的参数重新调用正确线程上的回调。您可以使用调用(阻止)或 BeginInvoke (非阻塞)方法重新调用方法。
    3. 下次调用该函数时, InvokeRequired 会返回false,因为我们现在位于正确的线程上并且每个人都很高兴。
    4. 这是解决此问题的一种非常紧凑的方法,可以使您的表单免受多线程事件回调的影响。

答案 2 :(得分:9)

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

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

答案 3 :(得分:2)

我对这个话题有点迟,但你可能想看看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; }
        }
    }
}

答案 4 :(得分:1)

作为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.";
}

答案 5 :(得分:0)

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