我在win form apps中大部分时间使用BackgroundWorker
来显示进度,因为我正在获取数据。我的印象是Work_completed
保证在主UI线程上执行,但事实并非如此。如果我们创建一个线程并在其中调用worker.RunWorkerAsync
,如果我们尝试更新任何gui控件,它就会中断。这是一个例子
private void StartButton_Click(object sender, EventArgs e)
{
Thread thread1 = new Thread(new ThreadStart(PerformWorkerTask));
_worker = new BackgroundWorker();
thread1.Start();
}
public void PerformWorkerTask()
{
_worker.DoWork += delegate
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(100);
}
};
_worker.RunWorkerCompleted += delegate
{
// this throws exception
MessageLabel.Text = "Completed";
};
_worker.RunWorkerAsync();
}
在这种情况下,我们如何让后台工作者工作?
答案 0 :(得分:1)
从你的例子中很难看出Thread(thread1)有什么好处,但是如果你真的需要这个thread1那么我认为你唯一的选择就是使用MainForm.Invoke()
来执行RunWorkerAsync()
(或者主线程上的一个小方法。
已添加:您可以使用以下内容:
Action a = new Action(_worker.RunWorkerAsync);
this.Invoke(a);
答案 1 :(得分:1)
RunWorkerAsync
通过从调用它的线程中获取SynchronizationContext
来实现其线程同步魔术。然后它保证事件将根据它获得的SynchronizationContext
的语义在正确的线程上执行。对于WindowsFormsSynchronizationContext
,即使用WinForms时自动使用的情况,通过发布到启动操作的线程的消息队列来同步事件。当然,这对你来说都是透明的,直到它破裂。
编辑:您必须从UI线程调用RunWorkerAsync
才能使其正常工作。如果您不能以任何其他方式执行此操作,最好的办法是在控件上调用操作的开头,以便在UI线程上启动worker:
private void RunWorker()
{
_worker = new BackgroundWorker();
_worker.DoWork += delegate
{
// do work
};
_worker.RunWorkerCompleted += delegate
{
MessageLabel.Text = "Completed";
};
_worker.RunWorkerAsync();
}
// ... some code that's executing on a non-UI thread ...
{
MessageLabel.Invoke(new Action(RunWorker));
}
答案 2 :(得分:1)
听起来问题只是你想要对GUI组件进行更改,而你实际上并不确定你是否在GUI线程上。 Dan发布了一种安全设置GUI组件属性的有效方法,但我发现以下快捷方法最简单:
MessageLabel.Invoke(
(MethodInvoker)delegate
{
MessageLabel.Text = "Hello World";
});
如果这种方法有任何问题,我想了解它们!
答案 3 :(得分:0)
在这里介绍的代码中,您将在与UI线程不同的线程中添加BackgroundWorker事件的委托。
尝试在主UI线程中添加事件处理程序,你应该没问题。
答案 4 :(得分:0)
我相信BackgroundWorker旨在自动使用新线程。因此,创建一个新线程只是为了调用RunWorkerAsync是多余的。您正在创建一个线程只是为了创建另一个线程。可能发生的是:
(以下建议使用VB而不是C#,因为这是我更熟悉的;我猜你可以弄清楚如何编写适当的C#代码来做同样的事情。)
摆脱无关的新线索;只需声明_worker WithEvents,向_worker.DoWork和_worker.RunWorkerCompleted添加处理程序,然后调用_worker.RunWorkerAsync而不是定义自定义的PerformWorkerTask函数。
编辑:要以线程安全的方式更新GUI控件,请使用以下代码(或多或少从this article from MSDN复制):
delegate void SetTextCallback(System.Windows.Forms.Control c, string t);
private void SafeSetText(System.Windows.Forms.Control c, string t)
{
if (c.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SafeSetText);
d.Invoke(d, new object[] { c, t });
}
else
{
c.Text = t;
}
}
答案 5 :(得分:0)
您可以通过以下方式使现有代码正常工作:
this.Dispatcher.BeginInvoke(() => MessageLabel.Text = "Completed")
而不是
MessageLabel.Text = "Completed"
您可能遇到跨线程数据访问问题,因此您必须确保在UI线程上访问MessageLabel的属性。这是一种方法。其他一些建议也是有效的。问自己的问题是:为什么要创建一个除了创建BackgroundWorker线程之外什么都不做的线程?如果有原因,那么很好,但是从你在这里显示的内容来看,没有理由你无法从事件处理程序创建和启动BackgroundWorker线程,在这种情况下,由于RunWorkerCompleted事件,不存在跨线程访问问题handler将在UI线程上调用其委托。
答案 6 :(得分:0)
处理这些通用问题的最佳方法是处理一次。在这里,我发布了一个包含backgroupdworker线程的小类,并确保workcompleted总是在UI线程上执行。
using System.Windows.Forms; namespace UI.Windows.Forms.Utilities.DataManagment { public class DataLoader { private BackgroundWorker _worker; private DoWorkEventHandler _workDelegate; private RunWorkerCompletedEventHandler _workCompleted; private ExceptionHandlerDelegate _exceptionHandler; public static readonly Control ControlInvoker = new Control();
public DoWorkEventHandler WorkDelegate { get { return _workDelegate; } set { _workDelegate = value; } } public RunWorkerCompletedEventHandler WorkCompleted { get { return _workCompleted; } set { _workCompleted = value; } } public ExceptionHandlerDelegate ExceptionHandler { get { return _exceptionHandler; } set { _exceptionHandler = value; } } public void Execute() { if (WorkDelegate == null) { throw new Exception( "WorkDelegage is not assinged any method to execute. Use WorkDelegate Property to assing the method to execute"); } if (WorkCompleted == null) { throw new Exception( "WorkCompleted is not assinged any method to execute. Use WorkCompleted Property to assing the method to execute"); } SetupWorkerThread(); _worker.RunWorkerAsync(); } private void SetupWorkerThread() { _worker = new BackgroundWorker(); _worker.WorkerSupportsCancellation = true; _worker.DoWork += WorkDelegate; _worker.RunWorkerCompleted += worker_RunWorkerCompleted; } void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if(e.Error !=null && ExceptionHandler != null) { ExceptionHandler(e.Error); return; } ControlInvoker.Invoke(WorkCompleted, this, e); } }
}
And here is the usage. One thing to note is that it exposes a static property ControlInvoker that needs to be set only once (you should do it at the beginning of the app load)
Let's take the same example that I posted in question and re write it
DataLoader loader = new DataLoader(); loader.ControlInvoker.Parent = this; // needed to be set only once
private void StartButton_Click(object sender, EventArgs e) {
Thread thread1 = new Thread(new ThreadStart(PerformWorkerTask)); _worker = new BackgroundWorker(); thread1.Start();
}
public void PerformWorkerTask() {
loader.WorkDelegate = delegate { // get any data you want for (int i = 0; i < 10; i++) { Thread.Sleep(100); } }; loader.WorkCompleted = delegate { // access any control you want MessageLabel.Text = "Completed"; }; loader.Execute();
}
干杯