пятница, 24 июля 2009 г.

Прерывание операции в BackgroundWorker

Уже не один раз на 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 необходимо выполнить следующие действия:
  1. Подписаться на событие DoWork, обработчик которого будет выполнять длительную операцию;
  2. Подписаться на событие RunWorkerCompleted, обработчик которого будет выполняться в потоке пользовательского интерфейса (если такой поток существует) и в котором необходимо выполнить обработку результатов выполнения операции. Операция может быть отменена (свойство Cancelled класса RunWorkerCompletedEventArgs будет равно true), завершена неудачно (свойство Error класса RunWorkerCompletedEventArgs будет содержать сгенерированное исключение), или успешно (тогда свойство Result класса RunWorkerCompletedEventArgs будет содержать возвращаемое значение, если такое существует).
  3. Вызвать метод 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.
Но прерывание потока в произвольной точке все еще может перевести приложение в рассогласованное состояние.

Комментариев нет:

Отправить комментарий