如何使事件回调在我的Win Forms线程安全?

39
当您在表单中订阅对象上的事件时,实际上是将控制权移交给事件源的回调方法。您不知道该事件源是否选择在不同的线程上触发事件。
问题在于当回调被调用时,您不能假定可以更新表单上的控件,因为有时这些控件会抛出异常,如果事件回调是在与表单运行所在的线程不同的线程上调用的话。
6个回答

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

18

以下是要点:

  1. 您无法从与创建它们(窗体的线程)不同的线程中进行 UI 控件调用。
  2. 委托调用(即事件钩子)在触发该事件的对象所在的线程上触发。

因此,如果您有一个单独的“引擎”线程执行某些工作,并且有一些 UI 监视状态更改,这些状态可以反映在 UI 中(例如进度条或其他任何东西),则会出现问题。引擎触发了一个对象更改事件,该事件已被表单挂钩。但是表单注册到引擎的回调委托将在引擎的线程上调用,而不是在表单的线程上调用。因此,您无法从该回调中更新任何控件。这可真让人沮丧!

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. 如果不是,则使用相同的参数在正确的线程上重新调用回调函数。您可以使用 Invoke (阻塞)或 BeginInvoke (非阻塞)方法重新调用方法。
  3. 下一次调用该函数时,InvokeRequired 返回 false,因为我们现在在正确的线程上,一切都很顺利。

这是一种非常紧凑的解决此问题并使您的窗体从多线程事件回调中安全的方式。


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

9

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

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

2

我有点晚了解这个主题,但您可能需要查看基于事件的异步模式。当正确实现时,它保证事件始终从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; }
        }
    }
}

1
我认为说“它保证事件总是从UI线程引发”有点误导人。更准确的说法不是它确保事件总是在UI线程上引发,而是确保事件处理程序在创建任务时使用的相同SynchronizationContext/线程上执行。(这可能不是UI线程/SynchronizationContext) - jspaey

2
作为一名“懒惰的程序员”,我有一个非常懒的做法来完成这个任务。
我所做的就是简单地这样:
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<object, EventArgs> 以及 lambda 表达式:Doinvoke(() => textLabel.Text = "Something") - Simon Gillbee

0
在许多简单的情况下,您可以使用MethodInvoker委托来避免创建自己的委托类型的需求。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接