Класс BackgroundWorker

150

Выполнять асинхронные операции можно разными способами. Ранее уже был показан один бесхитростный подход — создание нового объекта System.Threading.Thread вручную, применение асинхронного кода и запуск его методом Thread.Start(). Это мощный подход, потому что объект Thread ничего не задерживает. Можно создавать десятки потоков, устанавливать их приоритеты, управлять их состоянием (например, приостанавливать, возобновлять или прерывать их) и т.д.

Однако с этим подходом также связана некоторая опасность. При обращении к разделяемым данным понадобится применять блокировку для предотвращения тонких ошибок. Если потоки создаются часто или в большом количестве, то тем самым возникают дополнительные накладные расходы.

Приемы написания хорошего многопоточного кода и используемые при этом классы .NET не являются специфичными для WPF. В мире WPF может использоваться многопоточный код, аналогичный тому, что применяется в приложениях Windows Forms. Здесь же рассматривается один из наиболее простых и безопасных подходов: компонент System.ComponentModel.BackgroundWorker.

Класс BackgroundWorker появился в .NET 2.0 и был предназначен для упрощения работы с потоками в приложениях Windows Forms. Однако BackgroundWorker в той же мере применим и в WPF. Компонент BackgroundWorker предоставляет почти идеальный способ запуска длительно выполняющихся задач в отдельном потоке. Он использует диспетчер "за кулисами" и абстрагирует сложности маршализации с помощью модели событий.

Как вы убедитесь, BackgroundWorker также поддерживает два дополнительных удобства: события продвижения и сообщения отмены. В обоих случаях детали многопоточности скрыты, что облегчает кодирование.

BackgroundWorker незаменим, если есть единственная асинхронная задача, которая выполняется в фоновом режиме от начала до конца (с необязательной поддержкой уведомлений о продвижении и возможностью отмены). Если же имеется в виду что-то еще, например, асинхронная задача, которая работает на протяжении всей жизни приложения, или асинхронная задача, взаимодействующая с приложением, пока оно выполняет свою работу, то придется спроектировать специальное решение, воспользовавшись поддержкой многопоточности .NET.

Простая асинхронная операция

Чтобы попробовать BackgroundWorker в действии, стоит рассмотреть пример приложения. Базовым ингредиентом любого тестирования является длительно выполняющийся процесс. В следующем примере используется распространенный алгоритм нахождения простых чисел в заданном диапазоне, называемый решетом Эратосфена, который был изобретен Эратосфеном примерно в 240 г. до нашей эры.

В соответствие с этим алгоритмом, процесс начинается с составления списка всех целых чисел в заданном диапазоне. Затем вычеркиваются все числа, кратные всем простым числам, меньшим или равным квадратному корню из максимального числа. Оставшиеся числа и будут простыми.

Ниже приведена разметка окна и исходный код:

<Grid Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>


        <TextBlock Margin="5">От:</TextBlock>
        <TextBox Name="txtFrom" Grid.Column="1" Margin="5">1</TextBox>
        <TextBlock Grid.Row="1" Margin="5">До:</TextBlock>
        <TextBox Name="txtTo" Grid.Row="1" Grid.Column="1" Margin="5">500000</TextBox>

        <StackPanel Orientation="Horizontal" Grid.Row="2" Grid.Column="1">
            <Button Name="cmdFind" Margin="5" Padding="3" Click="cmdFind_Click">Найти простые числа</Button>
            <Button Name="cmdCancel" Margin="5" Padding="3" IsEnabled="False" Click="cmdCancel_Click">Остановить</Button>
        </StackPanel>

        <TextBlock Grid.Row="3" Margin="5">Результат:</TextBlock>
        <ListBox Name="lstPrimes" Grid.Row="3" Grid.Column="1" Margin="5"/>

        <ProgressBar Name="progressBar" Grid.Row="4" Grid.ColumnSpan="2" Margin="5" VerticalAlignment="Bottom" 
                     MinHeight="20" Minimum="0" Maximum="100" Height="20"/>
</Grid>
public class Worker
{
        public static int[] FindPrimes(int fromNumber, int toNumber)
        {
            // Найти простые числа в диапазоне между fromNumber 
            // и toNumber, вернув их в виде массива целых чисел
        }
}

Метод FindPrimes() принимает два параметра, которые ограничивают диапазон чисел. Затем код возвращает массив целых чисел, содержащий все простые числа из заданного диапазона.

Создание BackgroundWorker

Чтобы использовать BackgroundWorker, следует начать с создания его экземпляра. При этом на выбор доступны два подхода:

Оба подхода эквивалентны. Мы будем использовать второй подход. Первый шаг предусматривает обеспечение доступа к пространству имен System.ComponentModel в XAML-документе через импорт. Чтобы сделать это, понадобится отобразить пространство имен на префикс XML:

<Window ...
   xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"
   ...

Теперь можно создать экземпляр BackgroundWorker в коллекции Windows.Resources. При этом должно быть указано ключевое имя, чтобы позже можно было извлечь этот объект. В данном примере ключевым именем является backgroundWorker:

<Window.Resources>
        <cm:BackgroundWorker x:Key="backgroundWorker"></cm:BackgroundWorker>
</Window.Resources>

Преимущество объявления BackgroundWorker в разделе Window.Resources заключается в том, что можно установить его свойства и присоединить обработчики событий посредством атрибутов.

Например, ниже приведен дескриптор BackgroundWorker, который получится в конце примера, включающий поддержку уведомления о прохождении и возможности отмены, а также присоединяющий обработчики событий к DoWork, ProgressChanged и RunWorkerCompleted:

<cm:BackgroundWorker x:Key="backgroundWorker" 
      WorkerReportsProgress="True" WorkerSupportsCancellation="True"
      DoWork="backgroundWorker_DoWork" ProgressChanged="backgroundWorker_ProgressChanged" 
      RunWorkerCompleted="backgroundWorker_RunWorkerCompleted"/>

Чтобы получить доступ к этому ресурсу в коде, потребуется поместить его в коллекцию Resources. В данном примере окно выполняет этот шаг в своем конструкторе, так что весь код обработки событий легко доступен:

public partial class MainWindow : Window
{
        private BackgroundWorker backgroundWorker;

        public MainWindow()
        {
            InitializeComponent();
            backgroundWorker = ((BackgroundWorker)this.FindResource("backgroundWorker"));
        }
        
        ...

Запуск BackgroundWorker

Первый шаг к использованию BackgroundWorker с примером поиска простых чисел состоит в создании специального класса, который позволит передать входные параметры BackgroundWorker. При вызове BackgroundWorker.RunWorkerAsync() можете указать любой объект, который будет доставлен событию DoWorк. Тем не менее, допускается задавать только один объект, поэтому придется упаковать числа начала и конца диапазона в один класс, как показано ниже:

public class FindPrimesInput
{
        public int To
        { get; set; }

        public int From
        { get; set; }

        public FindPrimesInput(int from, int to)
        {
            To = to;
            From = from;
        }

}

Чтобы запустить сам BackgroundWorker, понадобится вызвать метод RunWorkerAsync() и передать объект FindPrimesInput. Ниже приведен код, который делает это, когда пользователь щелкает на кнопке Найти простые числа:

private void cmdFind_Click(object sender, RoutedEventArgs e)
{
            // Сделать недоступной эту кнопку и очистить предыдущие результаты
            cmdFind.IsEnabled = false; 
            cmdCancel.IsEnabled = true; 
            lstPrimes.Items.Clear(); 
            
            // Получить диапазон поиска
            int from, to; 
            if (!Int32.TryParse(txtFrom.Text, out from)) 
            { 
                MessageBox.Show("Неверное значение начала диапазона"); 
                return; 
            } 
            if (!Int32.TryParse(txtTo.Text, out to)) 
            {
                MessageBox.Show("Неверное значение конца диапазона");
                return; 
            } 
            
            // Начать поиск простых чисел в другом потоке
            FindPrimesInput input = new FindPrimesInput(from, to);
            backgroundWorker.RunWorkerAsync(input);
}

Когда BackgroundWorker начинает работу, он захватывает свободный поток из пула потоков CLR и затем инициирует событие DoWork из этого потока. В обработчике события DoWork запускается длительно выполняющаяся задача. Однако следует соблюдать осторожность и не обращаться к разделяемым данным (таким как поля класса окна) или объектам пользовательского интерфейса.

Как только работа будет завершена, BackgroundWorker инициирует событие RunWorkerCompleted, чтобы уведомить приложение. Это событие инициируется в потоке диспетчера, что позволит обратиться к разделяемым данным и пользовательскому интерфейсу, не порождая никаких проблем.

Как только BackgroundWorker захватывает поток, он инициирует событие DoWork. Это событие можно обработать, вызвав метод Worker.FindPrimes(). Событие DoWork предоставляет объект DoWorkEventArgs, который является ключевым ингредиентом при извлечении и возврате информации. Входной объект извлекается через свойство DoWorkEventArgs.Argument, а результат возвращается за счет установки свойства DoWorkEventArgs.Result:

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
            // Получить входные значения
            FindPrimesInput input = (FindPrimesInput)e.Argument;

            // Запустить поиск простых чисел и ждать. 
            // Это длительная часть работы, но она не подвешивает 
            // пользовательский интерфейс, поскольку выполняется в другом потоке
            int[] primes = Worker.FindPrimes(input.From, input.To);

            // Вернуть результат
            e.Result = primes;

}

Сам класс Worker, в котором рассчитываются простые числа, выглядит следующим образом:

class Worker
    {
        public static int[] FindPrimes(int fromNumber, int toNumber)
        {
            return FindPrimes(fromNumber, toNumber, null);
        }

        public static int[] FindPrimes(int fromNumber, int toNumber, System.ComponentModel.BackgroundWorker backgroundWorker)
        {
            int[] list = new int[toNumber - fromNumber];

            // Создать массив, содержащий все целые числа
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = fromNumber;
                fromNumber += 1;
            }

            // Числа, кратные всем простым числам, меньшим или равным квадратному 
            // корню из максимального числа отмечаем цифрой 0 - это обычные числа.
            // Все остальные отмечаем 1 - это простые числа
            int maxDiv = (int)Math.Floor(Math.Sqrt(toNumber));

            int[] mark = new int[list.Length];


            for (int i = 0; i < list.Length; i++)
            {
                for (int j = 2; j <= maxDiv; j++)
                {

                    if ((list[i] != j) && (list[i] % j == 0))
                    {
                        mark[i] = 1;
                    }

                }

                int iteration = list.Length / 100;
                if ((i % iteration == 0) && (backgroundWorker != null))
                {
                    if (backgroundWorker.CancellationPending)
                    {
                        // Возврат без какой-либо дополнительной работы
                        return null;
                    }

                    if (backgroundWorker.WorkerReportsProgress)
                    {
                        //float progress = ((float)(i + 1)) / list.Length * 100;
                        backgroundWorker.ReportProgress(i / iteration);
                        //(int)Math.Round(progress));
                    }
                }

            }

            // Cоздать новый массив, который содержит только простые числа, и вернуть этот массив
            int primes = 0;
            for (int i = 0; i < mark.Length; i++)
            {
                if (mark[i] == 0) primes += 1;

            }

            int[] ret = new int[primes];
            int curs = 0;
            for (int i = 0; i < mark.Length; i++)
            {
                if (mark[i] == 0)
                {
                    ret[curs] = list[i];
                    curs += 1;
                }
            }

            if (backgroundWorker != null && backgroundWorker.WorkerReportsProgress)
            {
                backgroundWorker.ReportProgress(100);
            }

            return ret;

        }
}

По завершении метода DoWork, BackgroundWorker инициирует RunWorkerCompletedEventArgs в потоке диспетчера. В этой точке можно извлечь результат из свойства Result. Затем можно обновить интерфейс и обратиться к переменным уровня окна без опаски:

private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
            if (e.Error != null) 
            { 
                // Ошибка была сгенерирована обработчиком события DoWork
                MessageBox.Show(e.Error.Message, "Произошла ошибка"); 
            } 
            else 
            {
                int [] primes = (int[])e.Result; 
                foreach (int prime in primes)
                    lstPrimes.Items.Add(prime);
            } 
            cmdFind.IsEnabled = true; 
            cmdCancel.IsEnabled = false; 
            progressBar.Value = 0;
}
Асинхронный поиск простых чисел

Обратите внимание, что не понадобился никакой код блокировки, и не было необходимости в использовании метода Dispatcher.BeginInvoke(). Объект BackgroundWorker обо всем позаботился сам.

"За кулисами" BackgroundWorker использует несколько многопоточных классов, которые появились в .NET 2.0, в том числе AsyncOperationManager, AsyncOperation и SynchronizationContext.

По сути, BackgroundWorker применяет AsyncOperationManager для управления фоновой задачей. AsyncOperationManager обладает встроенным интеллектом, а именно: он способен получить контекст синхронизации для текущего потока. В приложении Windows Forms AsyncOperationManager получает объект WindowsFormsSynchronizationContext, в то время как приложение WPF получает объект DispatcherSynchronizationContext. Концептуально эти классы выполняют одинаковую работу, но их внутреннее устройство отличается.

Отслеживание продвижения

BackgroundWorker также предоставляет встроенную поддержку первоначальной установки свойства BackgroundWorker.WorkerReportsProgress в true. На самом деле предоставление и отображение информации о продвижении — двухшаговый процесс.

Первым делом коду обработки события DoWork необходимо вызвать метод BackgroundWorker.ReportProgress() и показать предполагаемый процент готовности (от 0% до 100%). Это можно делать редко или часто — как нравится. При каждом вызове ReportProgress() объект BackgroundWorker инициирует событие ProgressChanged. На это событие можно отреагировать, чтобы прочитать процент готовности и обновить пользовательский интерфейс. Поскольку событие ProgressChanged инициировано в потоке пользовательского интерфейса, в применении Dispatcher.BeginInvoke() нет необходимости.

Метод FindPrimes() сообщает о продвижении с приращением 1%, используя код вроде показанного ниже:

...
int iteration = list.Length / 100;
if ((i % iteration == 0) && (backgroundWorker != null))
{
                    if (backgroundWorker.CancellationPending)
                    {
                        // Возврат без какой-либо дополнительной работы
                        return null;
                    }

                    if (backgroundWorker.WorkerReportsProgress)
                    {
                        //float progress = ((float)(i + 1)) / list.Length * 100;
                        backgroundWorker.ReportProgress(i / iteration);
                        //(int)Math.Round(progress));
                    }
}

Как только свойство BackgroundWorker.WorkerReportsProgress установлено, с этого момента можно реагировать на уведомления о продвижении, обрабатывая событие ProgressChanged. В этом примере индикатор продвижения обновляется соответствующим образом:

private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
     progressBar.Value = e.ProgressPercentage;
}

Придется также видоизменить обработчик события DoWork включив передачу методу FindPrimes() объекта BackgroundWorker. На рисунке ниже показан индикатор продвижения:

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
      ...
      int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker);
      ...
}
Отслеживание продвижения асинхронной задачи

Поддержка отмены

С помощью BackgroundWorker столь же просто добавить поддержку отмены длительно выполняющейся задачи. Первый шаг состоит в установке в true свойства BackgroundWorker.WorkerSupportsCancellation.

Чтобы запросить отмену, код должен вызвать метод BackgroundWorker.CancelAsync(). В этом примере отмена запрашивается при щелчке на кнопке Остановить:

private void cmdCancel_Click(object sender, RoutedEventArgs e)
{
            backgroundWorker.CancelAsync();
}

При вызове CancelAsync() ничего автоматически не происходит. Вместо этого код, выполняющий задачу, должен явно проверить запрос на отмену, выполнить необходимую очистку и вернуть управление. Ниже приведен код метода FindPrimes(), который проверяет запросы на отмену непосредственно перед сообщением о продвижении:

...
if (backgroundWorker.CancellationPending)
{
       // Возврат без какой-либо дополнительной работы
       return null;
}

Код в обработчике события DoWork также должен явно установить свойство DoWorkEventArgs.Cancel в true, чтобы завершить отмену. Затем производится возврат из метода без попытки построить строку простых чисел:

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
     ...
     int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker);

     if (backgroundWorker.CancellationPending)
     {
           e.Cancel = true;
           return;
     }

     // Вернуть результат
     e.Result = primes;
}

Даже при отмене операции событие RunWorkerCompleted все равно инициируется. В этой точке можно проверить, отменена ли задача, и предпринять соответствующую обработку:

private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
            if (e.Cancelled)
            {
                MessageBox.Show("Поиск отменен");
            } else if (e.Error != null) 
            { 
               ...

Теперь компонент BackgroundWorker позволяет запускать поиск и останавливать его принудительно.

Пройди тесты
Лучший чат для C# программистов