C# 如何使事件回调进入我的 win 表单线程安全?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/6184/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How do I make event callbacks into my win forms thread safe?
提问by Simon Gillbee
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. 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.
问题是当回调被调用时,你不能假设你可以在你的表单上创建更新控件,因为有时如果在与运行表单的线程不同的线程上调用事件回调,这些控件会抛出异常。
采纳答案by Jake Pearson
To simplify Simon's code a bit, you could use the built in generic Action delegate. 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.
为了稍微简化 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";
}
回答by Simon Gillbee
Here are the salient points:
以下是要点:
- You can't make UI control calls from a different thread than the one they were created on (the form's thread).
- Delegate invocations (ie, event hooks) are triggered on the same thread as the object that is firing the event.
- 您不能从与创建它们的线程不同的线程(表单的线程)进行 UI 控件调用。
- 委托调用(即事件挂钩)在与触发事件的对象相同的线程上触发。
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. The engine fire's an object changed event which has been hooked by the Form. But the callback delegate that the Form registered with the engine gets called on the engine's thread… not on the Form's thread. And so you can't update any controls from that callback. Doh!
因此,如果您有一个单独的“引擎”线程在做一些工作,并且有一些 UI 监视可以反映在 UI 中的状态更改(例如进度条或其他任何东西),那么您就会遇到问题。引擎触发的对象更改事件已被表单挂钩。但是在引擎的线程上调用了向引擎注册的表单的回调委托......而不是在表单的线程上。因此,您无法从该回调更新任何控件。哦!
BeginInvokecomes to the rescue. Just use this simple coding model in all your callback methods and you can be sure that things are going to be okay:
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";
}
It's quite simple really.
这真的很简单。
- Use InvokeRequiredto find out if this callback happened on the correct thread.
- 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.
- The next time the function is called, InvokeRequiredreturns false because we are now on the correct thread and everybody is happy.
- 使用InvokeRequired确定此回调是否发生在正确的线程上。
- 如果没有,则使用相同的参数在正确的线程上重新调用回调。您可以使用Invoke(阻塞)或BeginInvoke(非阻塞)方法重新调用方法。
- 下次调用该函数时,InvokeRequired返回 false,因为我们现在处于正确的线程上并且每个人都很高兴。
This is a very compact way of addressing this problem and making your Forms safe from multi-threaded event callbacks.
这是解决此问题并使您的表单免受多线程事件回调影响的一种非常紧凑的方法。
回答by Chris Farmer
In many simple cases, you can use the MethodInvoker delegate and avoid the need to create your own delegate type.
在许多简单的情况下,您可以使用 MethodInvoker 委托,而无需创建自己的委托类型。
回答by Jason Diller
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();
}
回答by OwenP
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; }
}
}
}
回答by Chase
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 内联在您的函数中或将其隐藏在单独的函数中来为您完成繁琐的工作。
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.";
}