Главная » Silverlight » Управление потоком

0

Когда базовая инфраструктура приложения готова, несложно добавить в нее инди­катор прогресса и средства отмены потока.

Чтобы организовать отмену потока, нужно добавить булево поле, сигнализирую­щее о необходимости прерывания. Код потока периодически проверяет значение бу­лева поля и, когда оно равно true, закрывает поток. Ниже приведен код, добавленный в класс ThreadWrapperBase.

// Флажок, сигнализирующий о необходимости закрыть поток private bool cancelRequested = false;

protected bool CancelRequested <

get { return cancelRequested; }

}

// Чтобы отменить задачу, нужно вызвать этот метод

public void RequestCancel ()

{

cancelRequested = true;

}

// Для отмены нужно вызвать метод OnCancelled (), // чтобы сгенерировать событие Cancelled public event EventHandler Cancelled;

protected void OnCancelled () {

if (Cancelled != null) Cancelled(this, EventArgs.Empty);

}

Ниже приведен код, добавленный в метод FindPrimesThreadWrapper. DoWork () для периодической проверки флажка отмены. Проверять флажок в каждой итерации не нужно. Проверка выполняется в одной из ста итераций.

int iteration = list.Length / 100;

if (і % iteration == 0) {

if (CancelRequested)

{

return;

}

)

Кроме того, нужно изменить метод ThreadWrapperBase.StartTaskAsync О , чтобы он распознавал, как был завершен поток: в результате отмены или нормального заверше­ния операции.

private void StartTaskAsync() {

DoTaskO ;

if (CancelRequested) {

status = StatusState.Cancelled; OnCancelled();

}

else {

status = StatusState.Completed; OnCompleted();

}

}

Необходимо также подключить обработчик события Cancelled и добавить кнопку Отмена. Ниже приведен код, инициирующий запрос отмены текущей задачи.

private void cmdCancel_Click(object sender,

RoutedEventArgs e)

{

threadWrapper.RequestCancel ();

}

А это — обработчик, выполняющийся при завершении отмены.

private void threadWrapper_Cancelled(object sender, EventArgs e)

{

this.Dispatcher.Beginlnvoke(delegate() { lblStatus.Text = "Поиск отменен."; cmdFind.IsEnabled = true; cmdCancel.IsEnabled = false;

}>;

}

Учитывайте, что поток Silverlight нельзя остановить с помощью метода Abort (), поэтому все, что можно сделать — это добавить в код потока условный оператор, про­веряющий состояние флажка, и Оператор, инициирующий завершение потока изнутри.

Класс BackgroundWorker

При использовании класса Thread потоки создаются вручную. Разработчик должен определить экземпляр объекта Thread, создать асинхронный код, организовать его вза­имодействие с приложением и запустить код на выполнение с помощью метода Thread. Start (). Такой подход весьма эффективен, потому что класс Thread ничего не скрывает от разработчика. Разработчик может создать десятки потоков, передать им информа­цию в любой момент времени, приостановить любой из них с помощью метода Thread. Sleep () и т.д. Однако данный подход также довольно опасен. При совместном обраще­нии к данным необходимо применять блокировки для предотвращения тонких ошибок. Если потоки создаются часто или в большом количестве, объекты потоков потребляют слишком много ресурсов.

Класс System. ComponentModel. BackgroundWorker предоставляет простой и безопасный способ создания потоков. Впервые он был добавлен в .NET 2.0 для упрощения создания потоков в приложениях Windows Fbrms. В классе BackgroundWorker неявно используется диспетчер потоков. Кроме того, класс BackgroundWorker перемещает средства диспетчери­зации за пределы модели событий, что существенно упрощает код.

Для облегчения кодирования все подробности создания потоков и управления ими в классе BackgroundWorker скрыты от разработчика. В класс добавлены средства индика­ции прогресса и отмены потока. Благодаря этому класс BackgroundWorker является наи­более практичным инструментом создания многопоточных приложений Silverlight.

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

Создание объекта BackgroundWorker

Для применения класса BackgroundWorker нужно создать его экземпляр в коде и программно подключить обработчики к событиям класса. Наиболее полезны события

DoWork, ProgressChanged и RunWorkerCompleted. Каждое рассматривается в следующем примере.

Совет. Для решения многих асинхронных задач довольно часто создают несколько объектов BackgroundWorker и сохраняют их в коллекции. В рассматриваемом далее примере используется один объект BackgroundWorker, создаваемый в коде в момент первой инициализации страницы.

Ниже приведен код инициализации, обеспечивающий поддержку индикатора про­гресса и средств отмены, а также подключающий обработчики к события. Этот код раз­мещен в конструкторе страницы BackgroundWorkerTest.

private BackgroundWorker backgroundWorker = new BackgroundWorker();

public BackgroundWorkerTest () {

InitializeComponent();

backgroundWorker.WorkerReportsProgress = true; backgroundWorker.WorkerSupportsCancellation = true; backgroundWorkef.DoWork += backgroundWorker_DoWork; backgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged; backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;

}

Выполнение потока BackgroundWorker

Рассмотрим пример использования класса BackgroundWorker для вычисления мас­сива простых чисел. Сначала нужно создать пользовательский класс, передающий входные параметры в объект BackgroundWorker. При вызове метода BackgroundWorker. RunWorkerAsync () ему можно передать любой объект. Этот объект будет автомати­чески предоставлен событию DoWork. Однако передать можно только один объект. Следовательно, чтобы передать два числа (границы диапазона), необходимо заключить их в один класс.

public class FindPrimesInput {

public int From { get; set; }

public int To { get; set; }

public FindPrimesInput(int from, int to) {

From = from; To = to;

)

Для запуска потока нужно вызвать метод BackgrounWorker .RunWorkerAsync () и пе­редать ему объект FindPrimeslnput. Ниже приведен код, выполняющий эту операцию после щелчка на кнопке cmdFind.

private void cmdFind_Click(object sender, RoutedEventArgs e)

{

// Отключение кнопки и очистка предыдущих результатов cmdFind.IsEnabled = false; cmdCancel.IsEnabled = true; IstPrimes.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;

}

// Запуск потока, вычисляющего простые числа FindPrimeslnput input = new FindPrimeslnput(from, to); backgroundWorker.RunWorkerAsync(input);

}

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

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

Всю трудоемкую работу выполняет обработчик события DoWork. Нужно быть аккурат­ным, чтобы не допустить совместного использования данных (например, полей других классов) или объектов интерфейса. При завершении работы объект BackgroundWorker генерирует событие RunWorkerCompleted, чтобы известить об этом приложение. Событие генерируется в потоке диспетчера, что позволяет обменяться данными с пользователь­ским интерфейсом, не порождая проблемы совместного использования данных.

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

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)

// Получение входных значений

FindPrimesInput input = (FindPrimesInput)е.Argument;

// Запуск потока

int[] primes = Worker.FindPrimes(input.From, input.To);

// Возвращение результата в пользовательский интерфейс

е.Result = primes;

)

При завершении метода объект BackgroundWorker генерирует событие RunWorkerCompleted в потоке диспетчера. В этот момент можно извлечь результат из свойства 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)

{

IstPrimes.Items.Add(prime);

}

}

cmdFind.IsEnabled = true;

cmdCancel.IsEnabled = false;

progressBar.Width = 0;

}

Обратите внимание на то, что блокировка не нужна. Кроме того, не используется метод Dispatcher .Beginlnvoke (). Объект BackgroundWorker взял всю "грязную" работу на себя.

Индикация прогресса

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

Чтобы установить в приложение индикатор прогресса, нужно присвоить значение true свойству BackgroundWorker .WorkerReportsProgress. Вывод информации о про­центе выполнения состоит из нескольких этапов. Сначала обработчик события DoWork должен вызвать метод BackgroundWorker .ReportProgress () и предоставить ему при­близительный процент выполнения (от 0 до 100). Метод ReportProgress можно вы­зывать в каждой итерации, однако, чтобы не терять время, обычно в коде вызывают его в каждой десятой или сотой итерации. Метод ReportProgress О генерирует со­бытие ProgressChanged. В обработчике события ProgressChanged можно прочесть текущий процент выполнения и обновить пользовательский интерфейс. Событие ProgressChanged генерируется в потоке пользовательского интерфейса, поэтому ис­пользовать метод Dispatcher .Beginlnvoke () нет необходимости.

Рассматриваемый в качестве примера метод FindPrimes () сообщает об изменении с шагом 1% с помощью следующего кода.

int iteration = list.Length / 100; for (int і = 0; і < list.Length; i++) (

// Событие генерируется, только когда произошло // изменение на 1%; кроме того событие не генерируется, / / если объекта BackgroundWorker нет или индикация // прогресса отключена if ((і % iteration == 0) SS (backgroundWorker != null) SS backgroundWorker.WorkerReportsProgress)

{

backgroundWorker.RepottProgress (i / iteration);

}

}

Примечание. Рабочий код должен иметь доступ к объекту BackgroundWorker, чтобы вызвать метод ReportProgress (). В данном примере конструктор класса FindPrimesWorker принимает ссылку на объект BackgroundWorker. Объект FindPrimewWorker использует BackgroundWorker для индикации прогресса и отмены потока.

Когда свойство BackgroundWorker.WorkerReportsProgress установлено, на из­менение можно отреагировать путем обработки события ProgressChanged. Однако в Silverlight (в отличие от полнофункциональной среды .NET) нет специализированно­го элемента управления, выводящего индикатор прогресса, поэтому его нужно создать вручную. Конечно, можно вывести процент выполнения в текстовом блоке, однако лучше имитировать графический индикатор прогресса с помощью встроенных эле­ментов управления Silverlight. В данном примере индикатор прогресса состоит из двух прямоугольников Rectangle (один — для вывода фона и второй — для отображения процента выполнения) и текстового блока TextBlock, содержащего числовое значение процента выполнения. Все три элемента размещены в одной ячейке Grid, поэтому они перекрываются.

<Rectangle x:Name="progressBarBackground" Fill="AliceBlue" Stroke="SlateBlue" Grid.Row="4" Grid.ColumnSpan="2" Margin="5" Height="30" /> <Rectangle x:Name="progressBar" Width="0"

HorizontalAlignment="Left" Grid.Row="4" Grid.ColumnSpan="2" Margin="5" Fill="SlateBlue" Height="30" />

<TextBlock x:Name="lblProgress" HorizontalAlignment="Center" Foreground="White" VerticalAlignment="Center" Grid.Row="4" Grid.ColumnSpan="2" />

Чтобы индикатор прогресса выглядел одинаково при разных значениях шири­ны окна браузера, необходим приведенный ниже код, который реагирует на событие SizeChanged и растягивает индикатор прогресса.

private double maxWidth;

private void DserControl_SizeChanged(object sender, SizeChangedEventArgs e)

maxWidth = progressBarBackground.ActualWidth;

}

Теперь необходимо обработать событие BackgroundWorker. ProgressChanged, отобра­жая текущий процент выполнения.

private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)

{

progressBar.Width =

(double)e.ProgressPercentage/100 * maxWidth; lblProgress.Text = ((double)e.ProgressPercentage/100).ToString("PO");

}

Кроме процента выполнения, через событие прогресса можно передать допол­нительную информацию. Для этого используется перегруженная версия метода ReportProgress (), принимающая два параметра. Первый параметр — процент вы­полнения, а второй — любой пользовательский объект, посредством которого можно передать дополнительную информацию. В примере с вычислением простых чисел че­рез него можно передать количество найденных чисел или последнее найденное число. Ниже приведен измененный код, задающий возвращение последнего найденного про­стого числа вместе с процентом выполнения.

backgroundWorker.ReportProgress(і / iteration, і);

if (e.UserState != null)

lblStatus.Text = "Последнее найденное простое число: " + е. UserState.ToString ();

Рис. 19.2. Индикатор прогресса, отображающий процент выполнения асинхронной задачи

На рис. 19.2 показан индикатор прогресса во время работы фонового потока.

В обработчике события ProgressChanged можно извлечь последнее число и вывести его на экран.

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

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

Чтобы отменить выполнение потока, код должен вызвать метод BackgroundWorker. CancelAsync (). Ниже приведен код вызова этого метода при щелчке на кнопке Отмена.

private void cmdCancel_Click(object sender, RoutedEventArgs e)

(

backgroundWorker.CancelAsync();

}

Однако при вызове метода CancelAsync О отмены потока не происходит. Метод всего лишь извещает поток о необходимости отмены. Код потока должен явно прове­рить наличие запроса на отмену, выполнить необходимые операции, включая очистку, и только после этого возвратить управление. Ниже приведен код метода FindPrimes О , проверяющий запрос на отмену непосредственно перед извещением о прогрессе.

for (int і = 0; і < list.Length; i++) {

if ( (i % iteration) && (backgroundWorker != null)) {

if (backgroundWorker.CancellationPending) {

// Возврат без обработки, return;

if (backgroundWorker.WorkerReportsProgress) {

backgroundWorker.ReportProgress (i / iteration);

}

}

)

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

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)

{

FindPrimesInput input = (FindPrimesInput)e.Argument; int[] primes = Worker.FindPrimes(input.From, input.To, backgroundWorker);

if (backgroundWorker.CancellationPending) {

e. Cancel = true; return ;

II Возвращение результатов e.Result = primes;

Событие RunWorkerCompleted генерируется, даже если задача отменена. В этот мо­мент можно проверить, была ли отменена задача, и выполнить необходимые операции.

private void backgroundWorker_RunWorkerCompleted(

object sender, RunWorkerCompletedEventArgs e)

{

if (e. Cancelled)

MessageBox.Show("Задача отменена");

else if (e.Error != null)

// Обработчик события DoWork генерирует ошибку

MessageBox.Show(e.Error.Message, "Произошла ошибка!");

else

int[] primes = (int[])e.Result;

foreach (int prime in primes)

{

IstPrimes.Items.Add(prime);

}

)

cmdFind.IsEnabled = true;

cmdCancel.IsEnabled = false;

progressBar.Value = 0;

}

Теперь объект BackgroundWorker позволяет как запустить операцию поиска простых чисел, так и отменить ее в процессе выполнения.

Резюме

В этой главе рассмотрены два мощных средства добавления многопоточности в при­ложение Silverlight. Конечно, возможность создания многопоточного приложения не означает, что каждое приложение нужно делать многопоточным. Многопоточность су­щественно усложняет приложение и при неаккуратном использовании может привести к тонким ошибкам, особенно если приложение выполняется в разных операционных системах и на разном оборудовании. Поэтому официальные руководства Microsoft ре­комендуют применять многопоточность, только тщательно взвесив все "за" и "против". Несомненно, многопоточность следует применять, когда в приложении есть длитель­ные операции, во время выполнения которых необходимо обеспечить постоянную го­товность интерфейса. В большинстве случаев лучше применить удобный высокоуров­невый класс BackgroundWorker, а не низкоуровневый класс Thread. Когда класс Thread все же необходим, не создавайте больше одного-двух фоновых потоков. Рекомендуется также обрабатывать в потоках разные части информации, как можно меньше взаимо­действующие друг с другом, чтобы избежать усложнений, связанных с блокировкой и синхронизацией.

Источник: Мак-Дональд, Мэтью. Silverlight 3 с примерами на С# для профессионалов. : Пер. с англ. —- М. : ООО «И.Д. Вильяме», 2010. — 656 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии