Как остановить BackgroundWorker на закрытии формы?
У меня есть форма, которая порождает BackgroundWorker, который должен обновить собственное текстовое поле формы (в основном потоке), следовательно, Invoke((Action) (...));
вызов.
Если в HandleClosingEvent
я просто делаю bgWorker.CancelAsync()
тогда я получаю ObjectDisposedException
на Invoke(...)
позвони, понятно. Но если я сижу в HandleClosingEvent
и дождитесь завершения bgWorker, после чего.Invoke(...) никогда не вернется, также понятно.
Любые идеи, как закрыть это приложение без исключения или тупик?
Ниже приведены 3 соответствующих метода простого класса Form1:
public Form1() {
InitializeComponent();
Closing += HandleClosingEvent;
this.bgWorker.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
while (!this.bgWorker.CancellationPending) {
Invoke((Action) (() => { this.textBox1.Text = Environment.TickCount.ToString(); }));
}
}
private void HandleClosingEvent(object sender, CancelEventArgs e) {
this.bgWorker.CancelAsync();
/////// while (this.bgWorker.CancellationPending) {} // deadlock
}
12 ответов
Я знаю, что единственный безопасный для исключительных ситуаций и исключительный способ сделать это - отменить событие FormClosing. Установите e.Cancel = true, если BGW все еще работает, и установите флаг, чтобы указать, что пользователь запросил закрытие. Затем проверьте этот флаг в обработчике событий RunWorkerCompleted BGW и вызовите Close(), если он установлен.
private bool closePending;
protected override void OnFormClosing(FormClosingEventArgs e) {
if (backgroundWorker1.IsBusy) {
closePending = true;
backgroundWorker1.CancelAsync();
e.Cancel = true;
this.Enabled = false; // or this.Hide()
return;
}
base.OnFormClosing(e);
}
void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
if (closePending) this.Close();
closePending = false;
// etc...
}
Я нашел другой способ. Если у вас есть больше backgroundWorkers, вы можете сделать:
List<Thread> bgWorkersThreads = new List<Thread>();
и в каждом методе DoWork backgroundWorker сделать:
bgWorkesThreads.Add(Thread.CurrentThread);
Артер, который вы можете использовать:
foreach (Thread thread in this.bgWorkersThreads)
{
thread.Abort();
}
Я использовал это в надстройке Word в элементе управления, который я использую в CustomTaskPane
, Если кто-то закроет документ или приложение раньше, все мои backgroundWorkes завершат свою работу, это вызовет COM Exception
(Точно не помню какой).CancelAsync()
не работает
Но с этим я могу закрыть все темы, которые используются backgroundworkers
Сразу в DocumentBeforeClose
событие и моя проблема решена.
Вот мое решение (извините, оно в VB.Net).
Когда я запускаю событие FormClosing, я запускаю BackgroundWorker1.CancelAsync(), чтобы установить для параметра CancellationPending значение True. К сожалению, программа никогда не получает возможность проверить значение CancellationPending, чтобы установить для e.Cancel значение true (что, насколько я могу судить, можно сделать только в BackgroundWorker1_DoWork). Я не убрал эту строку, хотя, похоже, это не имеет значения.
Я добавил строку, которая установит для моей глобальной переменной bClosingForm значение True. Затем я добавил строку кода в свой BackgroundWorker_WorkCompleted, чтобы проверить как e.Cancelled, так и глобальную переменную bClosingForm перед выполнением любых завершающих шагов.
Используя этот шаблон, вы должны иметь возможность закрыть свою форму в любое время, даже если специалист по фону находится в середине чего-то (что может быть не очень хорошо, но это обязательно произойдет, поэтому с ним также можно справиться). Я не уверен, если это необходимо, но вы можете полностью избавиться от фонового работника в событии Form_Closed после того, как все это произойдет.
Private bClosingForm As Boolean = False
Private Sub SomeFormName_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
bClosingForm = True
BackgroundWorker1.CancelAsync()
End Sub
Private Sub backgroundWorker1_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
'Run background tasks:
If BackgroundWorker1.CancellationPending Then
e.Cancel = True
Else
'Background work here
End If
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
If Not bClosingForm Then
If Not e.Cancelled Then
'Completion Work here
End If
End If
End Sub
Во-первых, исключение ObjectDisposedException является здесь только одной возможной ошибкой. Выполнение кода OP привело к следующему исключению InvalidOperationException в значительном количестве случаев:
Invoke или BeginInvoke не могут быть вызваны для элемента управления, пока не будет создан дескриптор окна.
Я полагаю, что это можно исправить, запустив работника по обратному вызову "Loaded", а не по конструктору, но всего этого сложного испытания можно избежать, если использовать механизм отчетов BackgroundWorker's Progress. Следующее работает хорошо:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
while (!this.bgWorker.CancellationPending)
{
this.bgWorker.ReportProgress(Environment.TickCount);
Thread.Sleep(1);
}
}
private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.textBox1.Text = e.ProgressPercentage.ToString();
}
Я как бы похитил процентный параметр, но один может использовать другую перегрузку для передачи любого параметра.
Интересно отметить, что удаление вышеуказанного спящего вызова приводит к засорению пользовательского интерфейса, потреблению высокой загрузки ЦП и постоянно увеличивает использование памяти. Я думаю, это как-то связано с перегруженной очередью сообщений графического интерфейса. Тем не менее, при неактивном режиме ожидания загрузка ЦП практически равна нулю, и использование памяти тоже выглядит нормально. Чтобы быть осторожным, возможно, следует использовать более высокое значение, чем 1 мс? Мнение эксперта здесь было бы оценено... Обновление: кажется, что пока обновление не слишком частое, оно должно быть в порядке: Ссылка
В любом случае, я не могу предвидеть сценарий, в котором обновление GUI должно происходить с интервалами короче пары миллисекунд (по крайней мере, в тех случаях, когда человек наблюдает за GUI), поэтому я думаю, что большую часть времени отчетность о ходе работы будет правильным выбором
Можете ли вы не ждать сигнала в деструкторе формы?
AutoResetEvent workerDone = new AutoResetEvent();
private void HandleClosingEvent(object sender, CancelEventArgs e)
{
this.bgWorker.CancelAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
while (!this.bgWorker.CancellationPending) {
Invoke((Action) (() => { this.textBox1.Text =
Environment.TickCount.ToString(); }));
}
}
private ~Form1()
{
workerDone.WaitOne();
}
void backgroundWorker1_RunWorkerCompleted( Object sender, RunWorkerCompletedEventArgs e )
{
workerDone.Set();
}
Я действительно не понимаю, почему DoEvents считается таким плохим выбором в этом случае, если вы используете this.enabled = false. Я думаю, что это сделало бы это довольно опрятным.
protected override void OnFormClosing(FormClosingEventArgs e) {
this.Enabled = false; // or this.Hide()
e.Cancel = true;
backgroundWorker1.CancelAsync();
while (backgroundWorker1.IsBusy) {
Application.DoEvents();
}
e.cancel = false;
base.OnFormClosing(e);
}
Это не будет работать для всех, но если вы делаете что-то в BackgroundWorker периодически, например, каждую секунду или каждые 10 секунд (возможно, опрос сервера), это работает хорошо, чтобы остановить процесс упорядоченным образом и без сообщений об ошибках. (по крайней мере, пока) и легко следовать;
public void StopPoll()
{
MyBackgroundWorker.CancelAsync(); //Cancel background worker
AutoResetEvent1.Set(); //Release delay so cancellation occurs soon
}
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
while (!MyBackgroundWorker.CancellationPending)
{
//Do some background stuff
MyBackgroundWorker.ReportProgress(0, (object)SomeData);
AutoResetEvent1.WaitOne(10000);
}
}
Ваш фоновый работник не должен использовать Invoke для обновления текстового поля. Он должен попросить поток пользовательского интерфейса обновить текстовое поле, используя событие ProgressChanged со значением для добавления в текстовое поле.
Во время события Closed (или, может быть, события Closing) поток пользовательского интерфейса запоминает, что форма закрыта, прежде чем он отменяет фонового работника.
После получения progressChanged поток пользовательского интерфейса проверяет, закрыта ли форма и только если нет, он обновляет текстовое поле.
Что насчет Me.IsHandleCreated?
Private Sub BwDownload_RunWorkerCompleted(sender As Object, e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BwDownload.RunWorkerCompleted
If Me.IsHandleCreated Then
'Form is still open, so proceed
End If
End Sub
Я передаю SynchronizationContext, связанный с текстовым полем, в BackgroundWorker и использую его для выполнения обновлений в потоке пользовательского интерфейса. Используя SynchronizationContext.Post, вы можете проверить, удаляется или удаляется элемент управления.
По-другому:
if (backgroundWorker.IsBusy)
{
backgroundWorker.CancelAsync();
while (backgroundWorker.IsBusy)
{
Application.DoEvents();
}
}
Одно решение, которое работает, но слишком сложно. Идея состоит в том, чтобы порождать таймер, который будет продолжать пытаться закрыть форму, и форма будет отказываться закрываться, пока не произойдет bgWorker
мертв.
private void HandleClosingEvent(object sender, CancelEventArgs e) {
if (!this.bgWorker.IsBusy) {
// bgWorker is dead, let Closing event proceed.
e.Cancel = false;
return;
}
if (!this.bgWorker.CancellationPending) {
// it is first call to Closing, cancel the bgWorker.
this.bgWorker.CancelAsync();
this.timer1.Enabled = true;
}
// either this is first attempt to close the form, or bgWorker isn't dead.
e.Cancel = true;
}
private void timer1_Tick(object sender, EventArgs e) {
Trace.WriteLine("Trying to close...");
Close();
}