Главная страница

лабараторная работа. Теория_к_лабораторным. 1. Применение параллельных вычислений


Скачать 0.7 Mb.
Название1. Применение параллельных вычислений
Анкорлабараторная работа
Дата24.10.2021
Размер0.7 Mb.
Формат файлаdoc
Имя файлаТеория_к_лабораторным.doc
ТипДокументы
#254886
страница5 из 9
1   2   3   4   5   6   7   8   9

6. “Гонка” потоков или как управлять экземплярами выделенных потоков


Этот и следующий примеры демонстрируют использование “персональных потоков” для управления параллельными вычислениями. С помощью экземпляра классаThread можно создать собственный выделенный поток исполнения, который не относится к пулу потоков, которым можно управлять посредством большого количества свойств и методов этого класса, и который можно завершить при первой необходимости. В приведенном ниже примере я не злоупотреблял всеми этими возможностями за исключением метода Sleep(), который позволяет приостановить на время работу выбранного потока. Делается это обычно для того, чтобы поток управления пользовательским интерфейсом смог выполнить или завершить выполнение обновлений элементов графического пользовательского интерфейсы. Помимо принудительной паузы также можно заставить основной поток дождаться окончания работы выбранного потока (метод Join()); изменить приоритет потока (свойство Priority) или прервать выполнение потока методом Abort(). Другие, более гибкие методы диспетчеризации потоков будут продемонстрированы на следующем шаге, а здесь хотелось бы просто показать, как создается и запускается коллекция потоков (коллекция экземпляров классаThread).

Пример простой: создается коллекция потоков, размер которой определяется пользователем, и каждому из экземпляров Thread в качестве исполняемого метода передается код, имитирующий разгон абстрактного транспортного средства. Все эти транспортные средства обладают схожей конструкцией, но различными характеристиками: мощность, максимальная скорость и количество передач. Величины этих характеристик формируются с использованием генератора случайных чисел. Я не питаю иллюзий, что поведение этих потоков будет близко к поведению реальных авто, но отдаленно, при условии, что у зрителя есть воображение, они их напомнить смогут. В процессе гонки напротив каждого гонщика, "визуализирующегося" с помощью элемента управления ProgressBar, отображается текущая передача (G), скорость (V) и ускорение (A). По ходу завершения гонки справа в списке формируется список “финишировавших”.


Пример работы программы, создающей собственные экземпляры потоков (Thread).

Это, собственно, цикл формирования коллекции потоков. Значение IsBackGround=true означает, что создаваемый поток является фоновым, и его завершение не приводит к завершению текущего процесса. И наоборот, все фоновые потоки завершаются автоматически с завершением всех основных потоков текущего процесса.

//Пример 4. 'Гонка' потоков

//Список элементов управления 'ProgressBar'

private List<ProgressBar> m_ThreadRaceBars;

//Список элементов управления 'TextBox' для вывода статистики

private List<TextBox> m_ThreadRaceTextBoxes;

//Количество 'финишировавших'

private int m_FinishIndex;

//Объект - блокировка для m_FinishIndex

private object m_FinishIndexLock = new object();

//'Старт гонки'

private void btnStartThreadRace_Click(object sender, EventArgs e)

{

//...

//<исходный код частично отсутствует>

//...

//Инициализация с использованием псевдо-случайных чисел и старт

System.Random _rnd = new System.Random();

for (int i = 0; i < _ThreadsCount; i++)

{

//Класс ThreadRaceCompetitor описывает 'участника гонки', содержит 'технические параметры гонщика'

//и определяет логику ускорения и переключения передач

ThreadRaceCompetitor _competitor; //индекс, максимальная скорость, кол-во передач, стартовое ускорение

_competitor = new ThreadRaceCompetitor(i, 1000 + _rnd.Next(800), 4 + _rnd.Next(4), 4 + _rnd.Next(3));

Thread _Thread = new Thread(new ParameterizedThreadStart(ThreadRaceProc));

//Все создаваемые потоки - фоновые и завершаются вместе с основным приложением

_Thread.IsBackground = true;

//Старт: ThreadRaceProc(_competitor)

_Thread.Start(_competitor);

}

}

А ниже код той самой процедуры, которая имитирует усилия гонщика добраться до финиша первым. Как и во всех предыдущих примерах для обновления элементов пользовательского интерфейса используется метод Invoke() и экземпляры MethodInvoker, принимающие в качестве параметра лямбда-выражение. Естественно, что конструктору MethodInvoker в качестве параметра можно передать и явный метод, но в данном примере мне кажется это лишним. Блокировка разделяемого ресурса m_FinishIndex выполнена с использованием оператора lock. Параметр оператора lock играет роль ключа: в тот момент, когда некий поток входит в критическую секцию - тело оператора lock (если вход свободен), то для всех остальных потоков блокируются ВСЕ критические секции, “помеченные” тем же параметром. Таким образом, все критические секции в программе можно разделить на ”независимые территории”, находиться в зоне которых могут одновременно несколько потоков. Получается, что ключ от критической секции определяется не только идентификатором потока, но и параметром входа, которым в нашем случае выступает переменная m_FinishIndexLock.

//Прохождение трассы отдельным участником

private void ThreadRaceProc(object state)

{

ThreadRaceCompetitor _competitor = (ThreadRaceCompetitor)state;

ProgressBar _bar = m_ThreadRaceBars[_competitor.Index];

TextBox _txt = m_ThreadRaceTextBoxes[_competitor.Index];

//Основной цикл

while (_bar.Value < _bar.Maximum)

{

//'жмем на газ'...

_competitor.Accelerate();



int _newValue = _bar.Value + _competitor.Speed;

//Обновление элементов управления в потоке пользовательского интерфейса

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

_bar.Invoke(new MethodInvoker(() =>

{

if (_newValue < _bar.Maximum)

_bar.Value = _newValue;

else

_bar.Value = _bar.Maximum;

_txt.Text = "G=" + _competitor.Gear.ToString()

+ "/" + _competitor.GearsCount.ToString() + ";V=" + _competitor.Speed.ToString()

+ "/" + _competitor.MaxSpeed.ToString()+ ";A=" + _competitor.Acceleration.ToString();

}));

Thread.Sleep(50);

}

//Финишируем...

lock(m_FinishIndexLock)

{

m_FinishIndex++;

//Обновляем список финишировавших в потоке пользовательского интерфейса

textBoxFinishList.Invoke(new MethodInvoker(() =>

{

textBoxFinishList.Text = textBoxFinishList.Text + m_FinishIndex.ToString()

+ ": " + (_competitor.Index +1).ToString()+ Environment.NewLine;

}));

}

}

Следует быть крайне осторожным с независимыми критическими секциями, поскольку довольно легко параллельные вычисления перевести в состояние deadlock(тупика), когда один поток, войдя в одну критическую секцию, не может его покинуть, поскольку по ходу выполнения кода этой критической секции натыкается на другую (вложенную) критическую секцию, заблокированную другим потоком. А этот “другой” поток может оказаться в это же самое время ровно в противоположной ситуации. С одной стороны, кажется, что попасть в ситуацию тупика сложно, поскольку явных вложенных критических секций никто не создает, но не стоит забывать, что у нас повсюду ООП с его инкапсуляцией. Доступ, например, к значению некоторого свойства класса может быть потокобезопасным и реализованным с использованием внутренней блокировки. Обозначим ее ключом “A”. Попытка изменения этого свойства в границах другой критической секции, ключ от которой обозначим “B”, легко может привести к такому тупику, если с использованием той же самой внутренней блокировки “A” в момент обращения к свойству уже что-то выполняется в другом параллельном потоке, и условием завершения выполнения этого “что-то” является вход на территории “B”. Мы заняли "B" и хотим войти еще и на территории "A", не покинув "B", а второй поток занял "A" и хочет доступ к "B"... Эти два потока, как “два барана” будут стоять до последнего, и вместо оптимизации скорости выполнения вашего кода вы получите "зависание" системы.

Весь код класса-гонщика здесь не представлен. Показан только метод “набора скорости” и “переключения передачи”. Код не изобилует сложными вычислениями, и все желающие могут его усовершенствовать.

//Обработка 'нажатия на газ'

public void Accelerate()

{

if (this.Speed > this.MaxSpeed)

return;

//Переключение передачи

if (this.Gear < this.GearsCount)

if (this.Speed > this.MaxSpeed * (double)this.Gear / this.GearsCount)

{

this.Gear++;

this.Acceleration++;



//Время, требующееся на переключение

Thread.Sleep(500);

}

//Изменение скорости

int _newSpeed = this.Speed + this.Acceleration;

if (_newSpeed > this.MaxSpeed)

this.Speed = this.MaxSpeed;

else

this.Speed = _newSpeed;

}
1   2   3   4   5   6   7   8   9


написать администратору сайта