Уже не один раз на rsdn.ru поднималось обсуждение такой темы: как прервать длительную асинхронную операцию, которая выполняется экземпляром класса BackgroundWorker? Именно на этот вопрос я и попытаюсь здесь ответить. Итак, класс BackgroundWorker предназначен для асинхронного выполнения длительных операций. Наиболее часто BackgroundWorker применяют для выполнения асинхронных операций при работе с пользовательским интерфейсом, хотя это и не обязательно. Преимущества применения этого класса с Windows Forms связано с тем, что класс BackgroundWorker является компонентом, с которым можно работать в Design-time, кроме того, событие завершения асинхронной операции сразу же вызывается в потоке пользовательского интерфейса, что упрощает взаимодействие с другими элементами управления. Рассмотрим простой способ применения класса BackgroundWorker (все примеры для простоты написаны в консольном приложении, а не в приложении Windows Forms, т.к. для рассматриваемого функционала это совершенно не важно):
var bw = new BackgroundWorker();
bw.DoWork += (o, e) =>
{
Console.WriteLine("{0}: Starting long running task",
DateTime.Now.TimeOfDay);
//Doing long running task
Thread.Sleep(TimeSpan.FromSeconds(10));
Console.WriteLine("{0}: Finishing long running task",
DateTime.Now.TimeOfDay);
};
bw.RunWorkerCompleted += (o, e) =>
{
//Handling worker complete event
if (e.Cancelled)
Console.WriteLine("{0}: Long running task cancelled",
DateTime.Now.TimeOfDay);
else if (e.Error != null)
Console.WriteLine("{0}: Long running task failed. Error: {1}",
DateTime.Now.TimeOfDay, e.Error);
else
Console.WriteLine("{0}: Long running task finished successfully",
DateTime.Now.TimeOfDay);
};
//Starting long running task
bw.RunWorkerAsync();
Console.ReadLine();
Для работы с экземпляром класса BackgroundWorker необходимо выполнить следующие действия:
- Подписаться на событие DoWork, обработчик которого будет выполнять длительную операцию;
- Подписаться на событие RunWorkerCompleted, обработчик которого будет выполняться в потоке пользовательского интерфейса (если такой поток существует) и в котором необходимо выполнить обработку результатов выполнения операции. Операция может быть отменена (свойство Cancelled класса RunWorkerCompletedEventArgs будет равно true), завершена неудачно (свойство Error класса RunWorkerCompletedEventArgs будет содержать сгенерированное исключение), или успешно (тогда свойство Result класса RunWorkerCompletedEventArgs будет содержать возвращаемое значение, если такое существует).
- Вызвать метод RunWorkerAsync и дождаться выполнения обработчиков событий.
Результат выполнения предыдущего примера будет следующий:
14:21:36.3437500: Starting long running task
14:21:46.3437500: Finishing long running task
14:21:46.3437500: Long running task finished successfully
Кроме базового сценария, класс BackgroundWorker поддерживает расширенные сценарии использования, которые включают отмену операции и уведомление о прогрессе выполнения. Рассмотрим следующий код:
var bw = new BackgroundWorker();
bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.DoWork += (o, e) =>
{
Console.WriteLine("{0}: Starting long running task",
DateTime.Now.TimeOfDay);
//Doing long running task
for (int i = 0; i < 10; i++)
{
//We can split our task in multiple stages
Thread.Sleep(TimeSpan.FromSeconds(1));
bw.ReportProgress((i + 1) * 10);
if (bw.CancellationPending)
{
//user canceled our task
Console.WriteLine("{0}: Task cancelled",
DateTime.Now.TimeOfDay);
break;
}
}
Console.WriteLine("{0}: Finishing long running task",
DateTime.Now.TimeOfDay);
};
bw.ProgressChanged += (o, e) =>
{
//Processing progress
Console.WriteLine("{0}: Progress changed. Percentage: {1}",
DateTime.Now.TimeOfDay, e.ProgressPercentage);
};
bw.RunWorkerCompleted += (o, e) =>
{
//Handling worker complete event
if (e.Cancelled)
Console.WriteLine("{0}: Long running task cancelled",
DateTime.Now.TimeOfDay);
else if (e.Error != null)
Console.WriteLine("{0}: Long running task failed. Error: {1}",
DateTime.Now.TimeOfDay, e.Error);
else
Console.WriteLine("{0}: Long running task finished successfully",
DateTime.Now.TimeOfDay);
};
//Starting long running task
bw.RunWorkerAsync();
Console.ReadLine();
//If we still running our task, trying to cancel it
if (bw.IsBusy)
bw.CancelAsync();
Console.ReadLine();
Результат, выводимый на консоль, будет зависеть от того, будет ли прервана асинхронная операция или нет.
Результат выполнения, при котором не было отмены операции:
10:48:06.2500000: Starting long running task
10:48:07.2500000: Progress changed. Percentage: 10
10:48:08.2500000: Progress changed. Percentage: 20
10:48:09.2500000: Progress changed. Percentage: 30
10:48:10.2500000: Progress changed. Percentage: 40
10:48:11.2500000: Progress changed. Percentage: 50
10:48:12.2500000: Progress changed. Percentage: 60
10:48:13.2500000: Progress changed. Percentage: 70
10:48:14.2500000: Progress changed. Percentage: 80
10:48:15.2500000: Progress changed. Percentage: 90
10:48:16.2500000: Finishing long running task
10:48:16.2500000: Progress changed. Percentage: 100
10:48:16.2500000: Long running task finished successfully
Результат выполнения, при котором операция была отменена:
10:55:22.8125000: Starting long running task
10:55:23.8281250: Progress changed. Percentage: 10
10:55:24.8281250: Progress changed. Percentage: 20
10:55:25.8281250: Progress changed. Percentage: 30
10:55:26.8281250: Progress changed. Percentage: 40
10:55:27.8281250: Progress changed. Percentage: 50
10:55:28.8281250: Progress changed. Percentage: 60
10:55:29.8281250: Task cancelled
10:55:29.8281250: Finishing long running task
10:55:29.8281250: Long running task cancelled
Класс BackgroundWorker поддерживает отмену длительной операции, но проблема в том, что не всегда существует возможность воспользоваться стандартным способом отмену операции, ведь для того чтобы отменить некоторую операцию необходимо каким-то способом прерывать ее выполнение и проверять свойство CancellationPending экземпляра BackgroundWorker. Решение этой задачи сводится к созданию класса-наследника от BackgroundWorker примерно следующего содержания.
public class AbortableBackgroundWorker : BackgroundWorker
{
private Thread workerThread;
protected override void OnDoWork(DoWorkEventArgs e)
{
workerThread = Thread.CurrentThread;
try
{
base.OnDoWork(e);
}
catch (ThreadAbortException)
{
e.Cancel = true; //We must set Cancel property to true!
Thread.ResetAbort(); //Prevents ThreadAbortException propagation
}
}
public void Abort()
{
if (workerThread != null)
{
workerThread.Abort();
workerThread = null;
}
}
}
Основная работа делается в переопределенном методе OnDoWork, который уже вызывается в потоке пула потоков (т.к. реализация класса BackgroundWorker основана на пуле потоков). В этом методе запоминается поток, выполняющий текущую операцию, и в методе Abort вызывается метод Abort этого потока. Прерывание потока путем вызова метода Thread.Abort имеет некоторые особенности, которые нужно учитывать, как при реализации класса AbortableBackgroundWorker, так и его клиентов.
Исключение ThreadAbortException является особенным типом исключения, которое трактуется CLR особым образом. Основная особенность в том, что после выполнения блоков catch и finally никакой код выполняться не будет, а учитывая, что прерываемый поток является потоком из пула потоков, то ни к чему хорошему это не приведет. Именно поэтому в обработчике исключения ThreadAbortException метода OnDoWork вызывается метод Thread.ResetAbort, который предотвращает распространение исключения ThreadAbortException и выполнения потока продолжается, но уже не с того места, где он выполнялся (в середине длительной операции), а так, как будто асинхронная операция успешно завершилась.
На клиентов класса AbortableBackgroundWorker, также налагаются некоторые ограничения, связанней с прерыванием рабочего потока. Во-первых, если обработчик события DoWork содержит обработку исключения, то там должно быть дополнительный блок catch для исключения ThreadAbortException, который обрабатывать на этом уровне не нужно. Во-вторых, исключение ThreadAbortException относится к асинхронным исключениями, которые могут возникнуть в любой точке управляемого кода, что может привести к непредвиденным последствиям и рассогласованному состоянию приложения. Например, если будет прерван поток, выполняющий статический конструктор некоторого класса, то последующее обращение к этому классу также будет приводить к генерации исключения TypeLoadException. В-третьих, CLR не всегда сможет прервать выполнение потока. Так, например, прерывание потока будет невозможно, если поток выполняет неуправляемый код или блоки catch/finally управляемого кода. Рассмотрим пример кода, с использованием класса AbortableBackgroundWorker:
var bw = new AbortableBackgroundWorker();
bw.DoWork += (o, e) =>
{
Console.WriteLine("{0}: Starting long running task",
DateTime.Now.TimeOfDay);
//Doing long running task
Thread.Sleep(TimeSpan.FromMinutes(1));
Console.WriteLine("{0}: Finishing long running task",
DateTime.Now.TimeOfDay);
};
bw.RunWorkerCompleted += (o, e) =>
{
//Handling worker complete event
if (e.Cancelled)
Console.WriteLine("{0}: Long running task cancelled",
DateTime.Now.TimeOfDay);
else if (e.Error != null)
Console.WriteLine("{0}: Long running task failed. Error: {1}",
DateTime.Now.TimeOfDay, e.Error);
else
Console.WriteLine("{0}: Long running task finished successfully",
DateTime.Now.TimeOfDay);
};
//Starting long running task
bw.RunWorkerAsync();
Console.ReadLine();
//If we still running our task, trying to abort it
if (bw.IsBusy)
bw.Abort();
Console.ReadLine();
Если в момент выполнения приложения нажать Enter, текущая операция будет прервана и результат выполнения приложения будет следующим: 14:15:17.8125000: Starting long running task
14:15:19.2343750: Long running task cancelled
Вместо заключения. Если вы в практической деятельности столкнетесь с необходимости прервать длительную операцию, которая выполняется с помощью класса BackgroundWorker, прежде всего подумайте о корректном способе прерывания операции путем разбития операции на этапы и проверки флага CancellationPending класса BackgroundWorker, т.к. это самый безопасный способ прервать длительную операцию. Но если вы столкнетесь с необходимостью прервать операцию, которая не может быть разбита на этапы, и вы знаете, что прерывание этой операции в произвольный момент времени не приведет к непредсказуемым последствиям, то можете воспользоваться механизмом описанным выше.
UPD.
Я допустил небольшую ошибку: в .Net Framework 2.0 и выше поток не будет прерван в следующих ситуациях:
- выполнение происходит внутри Constrained Execution Region (CER);
- выполняются блоки catch или finally;
- выполняется статический конструктор (.cctor);
- выполняется неуправляемый (unmanaged) код.
Поэтому проблема с тем, что поток может быть прерван во время выполнения статического конструктора, что приведет к невозможности использования типа существовала только в .Net Framework 1.1.
Но прерывание потока в произвольной точке все еще может перевести приложение в рассогласованное состояние.