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

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

提问人:Simon Gillbee 提问时间:8/9/2008 最后编辑:Ijas AmeenudeenSimon Gillbee 更新时间:1/20/2019 访问量:33800

问:

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

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

C# .NET WinForms 多线程处理 事件

评论


答:

18赞 Simon Gillbee 8/9/2008 #1

以下是要点:

  1. 不能从与创建这些控件的线程(窗体的线程)不同的线程进行 UI 控件调用。
  2. 委托调用(即事件挂钩)在与触发事件的对象相同的线程上触发。

因此,如果你有一个单独的“引擎”线程在做一些工作,并且有一些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";
}

这真的很简单。

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

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

评论

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

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

35赞 Jake Pearson 8/9/2008 #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";
}
9赞 Jason Diller 9/1/2008 #4

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

void SomethingHappened(object sender, EventArgs ea)
{
   MethodInvoker del = delegate{ textBox1.Text = "Something happened"; }; 
   InvokeRequired ? Invoke( del ) : del(); 
}
2赞 OwenP 12/5/2008 #5

我讲这个话题有点晚了,但你可能想看看基于事件的异步模式。如果实现得当,它可以保证始终从 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赞 jspaey 8/7/2012
我认为说“它保证事件始终从 UI 线程引发”有点误导。说它确保事件处理程序在创建任务的同一 SynchronizationContext / 线程上执行不是更准确吗?(可能不是 UI 线程/SynchronizationContext)
2赞 Chase 5/31/2012 #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.";
}

评论

0赞 Simon Gillbee 5/31/2012
我完全支持懒惰的编程:)如果您使用的是 .NET 3.5 或更高版本,则可以使用 lambda 表达式或与 lambda 表达式一起使用:ActionAction<object, EventArgs>Doinvoke(() => textLabel.Text = "Something")