如何正确停止后台工作者


我有一张表格,上面有两个盒子。并且我想根据 combobox1.Textcombobox2.Text填充 combobox2.DataSource(我假设用户已经在 combobox1中完成了输入,并且在 combobox2中输入)。所以我有一个 combobox2的事件处理程序,如下所示:

private void combobox2_TextChanged(object sender, EventArgs e)
{
if (cmbDataSourceExtractor.IsBusy)
cmbDataSourceExtractor.CancelAsync();


var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text,
V2 = combobox2.Text};
cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues );
}

至于构建 DataSource 是一个耗时的过程(它创建一个对数据库的请求并执行它) ,我认为最好使用 BackoundWorker 在另一个过程中执行它。因此,有一种情况是,cmbDataSource萃取器尚未完成其工作,而用户又键入了一个符号。在这个例子中,我在这一行得到了一个异常
cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues );关于 Background Worker 正忙,不能在同一时间执行多个操作。
如何消除这个异常?
先谢谢你!

218230 次浏览

CancelAsync doesn't actually abort your thread or anything like that. It sends a message to the worker thread that work should be cancelled via BackgroundWorker.CancellationPending. Your DoWork delegate that is being run in the background must periodically check this property and handle the cancellation itself.

The tricky part is that your DoWork delegate is probably blocking, meaning that the work you do on your DataSource must complete before you can do anything else (like check for CancellationPending). You may need to move your actual work to yet another async delegate (or maybe better yet, submit the work to the ThreadPool), and have your main worker thread poll until this inner worker thread triggers a wait state, OR it detects CancellationPending.

http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.cancelasync.aspx

http://www.codeproject.com/KB/cpp/BackgroundWorker_Threads.aspx

You will have to use a flag shared between the main thread and the BackgroundWorker, such as BackgroundWorker.CancellationPending. When you want the BackgroundWorker to exit, just set the flag using BackgroundWorker.CancelAsync().

MSDN has a sample: http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.cancellationpending.aspx

The problem is caused by the fact that cmbDataSourceExtractor.CancelAsync() is an asynchronous method, the Cancel operation has not yet completed when cmdDataSourceExtractor.RunWorkerAsync(...) exitst. You should wait for cmdDataSourceExtractor to complete before calling RunWorkerAsync again. How to do this is explained in this SO question.

If you add a loop between the CancelAsync() and the RunWorkerAsync() like so it will solve your problem

 private void combobox2_TextChanged(object sender, EventArgs e)
{
if (cmbDataSourceExtractor.IsBusy)
cmbDataSourceExtractor.CancelAsync();


while(cmbDataSourceExtractor.IsBusy)
Application.DoEvents();


var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text,
V2 = combobox2.Text};
cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues );
}

The while loop with the call to Application.DoEvents() will hault the execution of your new worker thread until the current one has properly cancelled, keep in mind you still need to handle the cancellation of your worker thread. With something like:

 private void cmbDataSourceExtractor_DoWork(object sender, DoWorkEventArgs e)
{
if (this.cmbDataSourceExtractor.CancellationPending)
{
e.Cancel = true;
return;
}
// do stuff...
}

The Application.DoEvents() in the first code snippet will continue to process your GUI threads message queue so the even to cancel and update the cmbDataSourceExtractor.IsBusy property will still be processed (if you simply added a continue instead of Application.DoEvents() the loop would lock the GUI thread into a busy state and would not process the event to update the cmbDataSourceExtractor.IsBusy)

I agree with guys. But sometimes you have to add more things.

IE

1) Add this worker.WorkerSupportsCancellation = true;

2) Add to you class some method to do the following things

public void KillMe()
{
worker.CancelAsync();
worker.Dispose();
worker = null;
GC.Collect();
}

So before close your application your have to call this method.

3) Probably you can Dispose, null all variables and timers which are inside of the BackgroundWorker.

MY example . DoWork is below:

    DoLengthyWork();


//this is never executed
if(bgWorker.CancellationPending)
{
MessageBox.Show("Up to here? ...");
e.Cancel = true;
}

inside DoLenghtyWork :

public void DoLenghtyWork()
{
OtherStuff();
for(int i=0 ; i<10000000; i++)
{  int j = i/3; }
}

inside OtherStuff() :

public void OtherStuff()
{
for(int i=0 ; i<10000000; i++)
{  int j = i/3; }
}

What you want to do is modify both DoLenghtyWork and OtherStuff() so that they become:

public void DoLenghtyWork()
{
if(!bgWorker.CancellationPending)
{
OtherStuff();
for(int i=0 ; i<10000000; i++)
{
int j = i/3;
}
}
}


public void OtherStuff()
{
if(!bgWorker.CancellationPending)
{
for(int i=0 ; i<10000000; i++)
{
int j = i/3;
}
}
}

In my case, I had to pool database for payment confirmation to come in and then update WPF UI.

Mechanism that spins up all the processes:

public void Execute(object parameter)
{
try
{
var url = string.Format("{0}New?transactionReference={1}", Settings.Default.PaymentUrlWebsite, "transactionRef");
Process.Start(new ProcessStartInfo(url));
ViewModel.UpdateUiWhenDoneWithPayment = new BackgroundWorker {WorkerSupportsCancellation = true};
ViewModel.UpdateUiWhenDoneWithPayment.DoWork += ViewModel.updateUiWhenDoneWithPayment_DoWork;
ViewModel.UpdateUiWhenDoneWithPayment.RunWorkerCompleted += ViewModel.updateUiWhenDoneWithPayment_RunWorkerCompleted;
ViewModel.UpdateUiWhenDoneWithPayment.RunWorkerAsync();
}
catch (Exception e)
{
ViewModel.Log.Error("Failed to navigate to payments", e);
MessageBox.Show("Failed to navigate to payments");
}
}

Mechanism that does checking for completion:

 private void updateUiWhenDoneWithPayment_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(30000);
while (string.IsNullOrEmpty(GetAuthToken()) && !((BackgroundWorker)sender).CancellationPending)
{
Thread.Sleep(5000);
}


//Plug in pooling mechanism
this.AuthCode = GetAuthToken();
}

Mechanism that cancels if window gets closed:

private void PaymentView_OnUnloaded(object sender, RoutedEventArgs e)
{
var context = DataContext as PaymentViewModel;
if (context.UpdateUiWhenDoneWithPayment != null && context.UpdateUiWhenDoneWithPayment.WorkerSupportsCancellation && context.UpdateUiWhenDoneWithPayment.IsBusy)
context.UpdateUiWhenDoneWithPayment.CancelAsync();
}

My answer is a bit different because I've tried these methods but they didn't work. My code uses an extra class that checks for a Boolean flag in a public static class as the database values are read or where I prefer it just before an object is added to a List object or something as such. See the change in the code below. I added the ThreadWatcher.StopThread property. for this explation I'm nog going to reinstate the current thread because it's not your issue but that's as easy as setting the property to false before accessing the next thread...

private void combobox2_TextChanged(object sender, EventArgs e)
{
//Stop the thread here with this
ThreadWatcher.StopThread = true;//the rest of this thread will run normally after the database function has stopped.
if (cmbDataSourceExtractor.IsBusy)
cmbDataSourceExtractor.CancelAsync();


while(cmbDataSourceExtractor.IsBusy)
Application.DoEvents();


var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text,
V2 = combobox2.Text};
cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues );
}

all fine

private void cmbDataSourceExtractor_DoWork(object sender, DoWorkEventArgs e)
{
if (this.cmbDataSourceExtractor.CancellationPending)
{
e.Cancel = true;
return;
}
// do stuff...
}

Now add the following class

public static class ThreadWatcher
{
public static bool StopThread { get; set; }
}

and in your class where you read the database

List<SomeObject>list = new List<SomeObject>();
...
if (!reader.IsDbNull(0))
something = reader.getString(0);
someobject = new someobject(something);
if (ThreadWatcher.StopThread == true)
break;
list.Add(something);
...

don't forget to use a finally block to properly close your database connection etc. Hope this helps! Please mark me up if you find it helpful.