This article has been localized into Russian by the community.
Реализация многопоточности с помощью BackgroundWorker
По умолчанию код, вызываемый приложением, выполняется в основном потоке этого приложения. Таким образом, во время работы этого кода не выполняются никакие другие действия, в том числе и обновление интерфейса.
Это вызывает удивление у тех, кто недавно занимается разработкой приложений для Windows и впервые сталкивается с тем, что выполнение сложных и длительных действий приводит к "зависанию" приложения. Результатом является появление огромного количества сообщений на форумах от расстроенных пользователей, которые запускают длительный процесс и используют индикатор прогресса, и в итоге видят, что индикатор не обновляется до тех пор, пока процесс не закончится.
Использование нескольких потоков позволяет решать такие проблемы. Однако, хотя C# дает возможность достаточно легко это реализовать, в многопоточности имеется множество подводных камней и многим людям трудно её понять. Для облегчения добавления дополнительного потока к приложению предназначен класс BackgroundWorker.
Как работает BackgroundWorker
Самой сложной особенностью многопоточности в приложениях Windows является то, что изменять элементы пользовательского интерфейса (UI) можно только из основного потока. Попытка сделать это из другого потока приводит к ошибке и немедленному завершению программы. Для изменения интерфейса необходимо вызвать метод, выполняющийся в главном потоке. Всё это выглядит весьма запутано, но использование BackgroundWorker упрощает эту задачу.
Обычно есть две причины обратиться к главному потоку приложения при выполнении действий в дополнительном потоке: обновить состояние приложения, чтобы отобразить прогресс выполнения задачи и, естественно, отобразить результат работы после её завершения. Эта идея лежит в основе класса BackgroundWorker, поэтому в нем представлены два события: ProgressChangedи RunWorkerCompleted.
Существует также третье событие, DoWork. В пределах этого события нельзя вносить изменения в пользовательский интерфейс. Вместо этого нужно вызвать метод ReportProgress(). Это приведет к появлению события ProgressChanged, в котором можно изменять UI. Как только задача завершена и установлен результат выполнения, возникает событие RunWorkerCompleted.
Таким образом, вся работа выполняется событиемDoWork. Весь код этого события выполняется в дополнительном потоке. Это и является причиной того, что этот код не может изменять пользовательский интерфейс. Вы передаете событию DoWork извне необходимые данные как аргумент метода RunWorkerAsync(), а затем возвращаете результат работы, присваивая его свойству e.Result.
В свою очередь, код событий ProgressChanged и RunWorkerCompleted выполняется в том же потоке, в котором был создан экземпляр BackgroundWorker. Обычно этот поток является главным потоком приложения и, соответственно, находясь в нем разрешается вносить изменения в интерфейс. Таким образом, единственным способом установить связь между фоновым заданием и пользовательским интерфейсом является обращение к методу ReportProgress().
Хотя использовать BackgroundWorker достаточно легко, необходимо четко понимать особенности его функционирования, чтобы не допустить каких-либо ошибок. Как было сказано ранее, ошибки при реализации многопоточности могут приводить к возникновению значительных проблем в работе приложения.
Пример использования BackgroundWorker
Применим полученные знания. Используем BackgroundWorker для выполнения простого, но длительного задания. Проверим, делится ли на 7 каждое число из диапазона от 0 до 10000. Для современных компьютеров это не является сложной задачей, поэтому добавим миллисекундную задержку на каждой итерации.
Наше приложение будет содержать две кнопки: при нажатии на первую задача выполняется синхронно, т.е. в основном потоке, при нажатии на вторую - с помощью BackgroundWorker, т.е. в дополнительном потоке. Это позволит легко определить, существует ли необходимость в использовании дополнительного потока при выполнении длительных операций. Код приложения:
<Window x:Class="WpfTutorialSamples.Misc.BackgroundWorkerSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BackgroundWorkerSample" Height="300" Width="375">
<DockPanel Margin="10">
<DockPanel DockPanel.Dock="Top">
<Button Name="btnDoSynchronousCalculation" Click="btnDoSynchronousCalculation_Click" DockPanel.Dock="Left" HorizontalAlignment="Left">Synchronous (same thread)</Button>
<Button Name="btnDoAsynchronousCalculation" Click="btnDoAsynchronousCalculation_Click" DockPanel.Dock="Right" HorizontalAlignment="Right">Asynchronous (worker thread)</Button>
</DockPanel>
<ProgressBar DockPanel.Dock="Bottom" Height="18" Name="pbCalculationProgress" />
<ListBox Name="lbResults" Margin="0,10" />
</DockPanel>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
namespace WpfTutorialSamples.Misc
{
public partial class BackgroundWorkerSample : Window
{
public BackgroundWorkerSample()
{
InitializeComponent();
}
private void btnDoSynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
int max = 10000;
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
int result = 0;
for(int i = 0; i < max; i++)
{
if(i % 42 == 0)
{
lbResults.Items.Add(i);
result++;
}
System.Threading.Thread.Sleep(1);
pbCalculationProgress.Value = Convert.ToInt32(((double)i / max) * 100);
}
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + result);
}
private void btnDoAsynchronousCalculation_Click(object sender, RoutedEventArgs e)
{
pbCalculationProgress.Value = 0;
lbResults.Items.Clear();
BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += worker_DoWork;
worker.ProgressChanged += worker_ProgressChanged;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
worker.RunWorkerAsync(10000);
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
int max = (int)e.Argument;
int result = 0;
for(int i = 0; i < max; i++)
{
int progressPercentage = Convert.ToInt32(((double)i / max) * 100);
if(i % 42 == 0)
{
result++;
(sender as BackgroundWorker).ReportProgress(progressPercentage, i);
}
else
(sender as BackgroundWorker).ReportProgress(progressPercentage);
System.Threading.Thread.Sleep(1);
}
e.Result = result;
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pbCalculationProgress.Value = e.ProgressPercentage;
if(e.UserState != null)
lbResults.Items.Add(e.UserState);
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("Numbers between 0 and 10000 divisible by 7: " + e.Result);
}
}
}
Код XAML содержит определения для двух кнопок: одна для запуска задачи синхронно (в главном потоке), вторая - для запуска задачи асинхронно (в фоновом потоке). Также в окне располагаются ListBox для отображения всех найденных чисел и ProgressBar для отображения собственно прогресса выполнения задачи.
Программный код начинается с описания обработчика синхронного события. В коде последовательно перебираются числа от 0 до 10000 с небольшой задержкой на каждой итерации. Далее, если текущее число делится на 7, оно добавляется в список ListBox. Также на каждой итерации происходит обновление положения ProgressBar. После окончания работы выводится сообщение с количеством найденных чисел.
Если запустить приложение и нажать первую кнопку, окно примет следующий вид:
Список пуст, положение ProgressBar не изменяется, кнопка остается в нажатом состоянии. Всё это говорит о том, что с момента нажатия на кнопку пользовательский интерфейс ни разу не обновлялся.
Нажатие на вторую кнопку запускает процесс решения задачи с помощью BackgroundWorker. Как можно заметить, выполняются практически те же самые действия, но другим способом. Вся работа теперь выполняется в обработчике DoWork, вызов которого происходит после обращения к методу RunWorkerAsync(). Позже будет рассмотрена передача данных с помощью этого метода.
Внутри обработчика DoWork нельзя осуществлять обновление пользовательского интерфейса, для этого необходимо вызвать метод ReportProgress. Если текущее число делится на 7, то оно добавляется в список, иначе просто производится расчет прогресса выполнения задачи для обновления ProgressBar.
Как только все числа будут проверены, свойству e.Result будет присвоено значение - результат работы. Далее вызывается событие RunWorkerCompleted, в обработчике которого результат будет показан пользователю. Казалось бы, проще показать пользователю результат непосредственно после выполнения работы. Однако, использованный способ позволяет гарантировать, что внутри события DoWorkне произойдет обращения к пользовательскому интерфейсу.
В результате пользовательский интерфейс, как вы видите, намного более дружественный:
Интерфейс стал отзывчивым: приложение больше не "зависает", кнопка правильно реагирует на нажатие, в список номеров добавляются значения, положение ProgressBar меняется.
Ввод и вывод
Заметьте, что и входные данные в виде аргумента, передаваемого методу RunWorkerAsync(), и выходные данные в виде значения свойства e.Result события DoWork, имеют тип object. Это означает, что вы можете передавать значения любого типа. В рассмотренном примере входные и выходные данные представляют собой целые числа, но использование более сложных данных - вполне обычное явление.
В таком случае передаются сложные типы данных, например, структуры или классы. Это позволяет передавать сколько угодно сложные данные между интерфейсом приложения и BackgroundWorker.
Это справедливо и для метода ReportProgress. Аргумент этого метода под названием userState имеет тип object и может использоваться для передачи любых данных методу ProgressChanged.
Заключение
BackgroundWorker является прекрасным инструментом для реализации многопоточности, в основном из-за простоты использования. В этой статье мы рассмотрели одну из функций BackgroundWorker - отслеживание прогресса выполнения. Было бы полезно иметь ещё и возможность останавливать выполнение задачи. Этот вопрос будет рассмотрен в следующей статье.