TOC

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 - отслеживание прогресса выполнения. Было бы полезно иметь ещё и возможность останавливать выполнение задачи. Этот вопрос будет рассмотрен в следующей статье.


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!