Студопедия

КАТЕГОРИИ:

АвтоАвтоматизацияАрхитектураАстрономияАудитБиологияБухгалтерияВоенное делоГенетикаГеографияГеологияГосударствоДомЖурналистика и СМИИзобретательствоИностранные языкиИнформатикаИскусствоИсторияКомпьютерыКулинарияКультураЛексикологияЛитератураЛогикаМаркетингМатематикаМашиностроениеМедицинаМенеджментМеталлы и СваркаМеханикаМузыкаНаселениеОбразованиеОхрана безопасности жизниОхрана ТрудаПедагогикаПолитикаПравоПриборостроениеПрограммированиеПроизводствоПромышленностьПсихологияРадиоРегилияСвязьСоциологияСпортСтандартизацияСтроительствоТехнологииТорговляТуризмФизикаФизиологияФилософияФинансыХимияХозяйствоЦеннообразованиеЧерчениеЭкологияЭконометрикаЭкономикаЭлектроникаЮриспунденкция

Private void Form1_Load(object sender, EventArgs e)




Лабораторная работа №8. Разработка многопоточных приложений

 

Цель работы

Изучить методы и средства, пространства имен и классы, применяемые при работе с потоками в среде .NET Framework. Получить практические навыки разработки многопоточных приложений на языке C#.

 

Сведения из теории

Общие сведения по работе с потоками

Многопоточная программа состоит из двух или более частей, которые могут выполняться одновременно. Каждая часть такой программы называется потоком, и каждый поток определяет собственный путь выполнения инструкций. Таким образом, многопоточность представляет собой специальную форму многозадачности.

Различают два вида многозадачности: с ориентацией на процессы и с ориентацией на потоки. Процесс по сути представляет собой выполняемую программу. Следовательно, многозадачность, ориентированная на процессы, – это средство, позволяющее компьютеру выполнять две или больше программ одновременно.

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

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

В среде .NET Framework определено два типа потоков: высокоприоритетный (foreground) и низкоприоритетный, или фоновый (background). По умолчанию поток создается высокоприоритетным, но его тип можно изменить, т.е. сделать фоновым. Единственное различие между высоко- и низкоприоритетным потоками состоит в том, что последний будет автоматически завершен, если все высокоприоритетные потоки в его процессе остановились.

Все процессы имеют по крайней мере один поток управления, который обычно называется основным, поскольку именно с этого потока начинается выполнение программы. Из основного можно создать и другие потоки. Желательно (но не обязательно), чтобы в многопоточной программе основной поток выполнялся последним.

Классы, которые поддерживают многопоточное программирование, определены в пространстве имен System.Threading.

Многопоточная система C# встроена в класс Thread, который инкапсулирует поток управления. Класс Thread является sealed-классом, т.е. он не может иметь наследников. В классе Thread определен ряд методов и свойств для управления потоками.

Чтобы создать поток, необходимо создать объект типа Thread. В классе Thread определен следующий конструктор:

Public Thread(ThreadStart entryPoint)

Здесь параметр entryPoint содержит имя метода, который будет вызван, чтобы начать выполнение потока. Тип ThreadStart – это делегат (ссылка на метод), определенный в среде .NET Framework:

Public delegate void ThreadStart()

Итак, начальный метод должен иметь тип возвращаемого значения void и не принимать никаких аргументов.

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

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

 

Статический компонент Назначение
CurrentThread Это свойство только для чтения возвращает ссылку на поток, выполняемый в настоящее время
Sleep() Приостанавливает выполнение текущего потока на указанное пользователем время

 

Обычные (нестатические) компоненты класса Thread приведены в следующей таблице:

 

Компонент Назначение
IsAlive Возвращает true или false в зависимости от того, запущен поток или нет
IsBackground Определяет, является ли поток фоновым
Name Текстовое имя потока
Priority Позволяет получить/установить приоритет потока (используются значения из перечисления ThreadPriority)
ThreadState Возвращает информацию о состоянии потока (используются значения из перечисления ThreadState)
Interrupt() Прерывает работу текущего потока
Join() Ждет появления другого потока (или указанный промежуток времени) и завершается
Resume() Продолжает работу после приостановки работы потока
Start() Начинает выполнение потока, определенного делегатом ThreadStart
Suspend() Приостанавливает выполнение потока. Если выполнение потока уже приостановлено, то игнорируется

 

Синхронизация потоков

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

В основе синхронизации лежит понятие блокировки, т.е. управление доступом к некоторому блоку кода в объекте. На то время, когда объект заблокирован одним потоком, никакой другой поток не может получить доступ к заблокированному объекту. Когда поток снимет блокировку, объект станет доступным для использования другим потоком.

Синхронизация поддерживается ключевым словом lock. Формат использования:

Lock(object)

{

//инструкции, подлежащие синхронизации

}

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

 

Взаимодействие потоков

Рассмотрим следующую ситуацию. Поток (назовем его Т) выполняет содержимое lock-блока и требует доступа к ресурсу (назовем его R), который временно недоступен. Что делать потоку Т? Если поток Т войдет в цикл опроса в ожидании доступности ресурса R, он свяжет объект, блокируя доступ к нему другим потокам. Это решение трудно назвать оптимальным, поскольку оно аннулирует преимущества программирования в многопоточной среде. Будет лучше, если поток Т временно откажется от «претензий» на объект, позволив другому потоку выполнить свою работу. Когда же ресурс R станет доступным, поток Т можно уведомить об этом, и он возобновит выполнение. Такой подход опирается на межпоточные средства общения, которые позволяют одному потоку уведомить другой о том, что он блокируется, а затем первого поставить в известность о том, что он может возобновить выполнение. C# поддерживает межпоточное взаимодействие с помощью методов Wait(), Pulse() и PulseAll(), определенных в классе Monitor. Эти методы можно вызывать только внутри lock-блока.

Когда выполнение потока временно блокируется, вызывается метод Wait(), т.е. он переходит в режим ожидания («засыпает») и снимает блокировку с объекта, позволяя другому потоку использовать этот объект. Позже, когда другой поток входит в аналогичное состояние блокирования и вызывает метод Pulse() или PulseAll(), «спящий» поток «просыпается». Обращение к методу Pulse() возобновляет выполнение потока, стоящего первым в очереди потоков, пребывающих в режиме ожидания. Обращение к методу PulseAll() сообщает о снятии блокировки всем ожидающим потокам.

При разработке многопоточных программ необходимо позаботиться о том, чтобы во время их выполнения не создалась тупиковая ситуация, вызванная взаимоблокировкой. При взаимоблокировке один поток ожидает, пока другой не выполнит некоторое действие, но в то же время второй поток ожидает действия первого. Таким образом, оба потока приостановлены, ожидая друг друга, и ни один из них не выполняется. Как правило, если многопоточная программа вдруг «виснет», то наиболее вероятная причина этого – взаимоблокировка.

Пример выполнения работы

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

Внешний вид работающего приложения приведен на рисунке 8.1.

 

Рис. 8.1. Пример работающего приложения

 

Создание потоков

Прежде всего, для работы с потоками необходимо подключить пространство имен System.Threading.

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

В этом методе сначала создается контекст графического устройства и две кисти: одна (красного цвета) для круга, вторая (цвета формы) – для стирания предыдущего изображения круга.

Чтобы два круга в разных потоках не сливались друг с другом, для них указываются разные координаты x. Значения координаты устанавливаются в зависимости от имени потока (свойство Name).

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

После окончания цикла для корректности выводится сообщение о завершении текущего потока. Листинг метода приведен ниже:

 

Private void MoveCircle()

{

//создаем контекст устройства

Graphics g = this.CreateGraphics();

//создаем красную кисть - для круга

Brush b1 = Brushes.Red;

//и кисть цвета формы - для стирания круга

Brush b2 = SystemBrushes.Control;

int x;

//координата x круга будет зависеть от имени потока

if (Thread.CurrentThread.Name == "First")

   x = Width / 2 - 30;

else

   x = Width / 2 + 30;

//цикл от верхнего до нижнего края формы

for (int y = 10; y < Height - 40; y++)

{

   //рисуем круг

   g.FillEllipse(b1, x - 10, y - 10, 20, 20);

   //"усыпляем" поток на 30 миллисекунд

   Thread.Sleep(30);

   //стираем круг

   g.FillEllipse(b2, x - 10, y - 10, 20, 20);

}

//проверяем корректность завершения потока

MessageBox.Show("Поток " + Thread.CurrentThread.Name + " завершен!");

}

 

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

 

private void Form1_Load(object sender, EventArgs e)

{

//создаем первый поток

//(точка входа в него - метод MoveCircle())

Thread thread1 = new Thread(new ThreadStart(MoveCircle));

//присваиваем потоку имя

thread1.Name = "First";

//аналогично для второго потока

Thread thread2 = new Thread(new ThreadStart(MoveCircle));

thread2.Name = "Second";

//запускаем оба потока

thread1.Start();

thread2.Start();

}

 










Последнее изменение этой страницы: 2018-06-01; просмотров: 274.

stydopedya.ru не претендует на авторское право материалов, которые вылажены, но предоставляет бесплатный доступ к ним. В случае нарушения авторского права или персональных данных напишите сюда...