Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

u_course

.pdf
Скачиваний:
39
Добавлен:
04.06.2015
Размер:
1.87 Mб
Скачать

Средства разработки параллельных программм

51

<await (arrive[j] > arrive[i]);>;

Если итераций неограниченное число, есть опасность переполнения флагов, поскольку теперь их никто не сбрасывает. Однако на практике вероятность переполнения невелика.

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

Например, в вычислительной системе с распределенной памятью Cray T3D/T3E разработана аппаратная барьерная синхронизация по принципу дихотомии (см. рис. 2.5). В схеме поддержки каждого процессорного элемента (ПЭ) предусмотрено несколько входных и выходных регистров синхронизации. Каждый разряд этих регистров соединен со своей независимой цепью реализации барьера. Все цепи синхронизации одинаковы, а их общее число зависит от конфигурации компьютера. Каждая цепь строится по принципу двоичного дерева на основе двух типов устройств. Одни устройства реализуют логическое умножение (&), другие дублируют сигнал с единственного входа на два выхода.

ПЭ_0

 

ПЭ_1

 

ПЭ_2

 

ПЭ_3

 

ПЭ_4

 

ПЭ_5

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

&

1-2

 

&

1-2

 

&

1-2

 

 

 

 

 

 

 

 

 

ПЭ_6 ПЭ_7

&

1-2

 

 

 

 

 

 

 

&

1-2

 

&

1-2

 

 

 

 

 

 

ПЭ – процессорный

 

 

 

 

 

Элемент 1-2 – уст-

 

&

1-2

 

элемент

 

 

ройство дублирова-

 

 

 

 

 

Элемент & – устройст-

 

 

 

 

 

ния входа на два вы-

во логического умно-

 

 

 

 

 

хода

жения

 

 

 

 

 

 

Рис. 2.5 Барьерная синхронизация в компьютере Cray T3D/T3E

Средства разработки параллельных программм

52

СЕМАФОРЫ

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

Семафор это особый тип разделяемой переменной, которая обрабатывается только двумя неделимыми операциями P() и V(). Семафор можно считать экземпляром класса «семафор», операции P() и V() – методами этого класса с дополнительным атрибутом, определяющим их неделимость. Значе-

ние семафора – неотрицательное целое число.

Операция V() используется для сигнализации, что событие произошло,

поэтому она увеличивает значение семафора на единицу.

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

В нашей нотации будем объявлять семафоры следующим образом:

sem s;

По умолчанию значение семафора 0, но он может быть проинициализирован любым положительным значением:

sem s=1;

Возможно объявление и инициализация массива семафоров:

sem forks[10] = ( [10] 0 );

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

P(s): <await (s>0) s=s-1;> V(s): <s=s+1;>

Операция P() гарантирует неотрицательность семафора.

Пусть s=1. Если два потока одновременно пытаются выполнить операцию P(s), то это удастся только одному из них. Если один поток пытается выполнить операцию V(s), а другой P(s), то они будут обе выполнены, но в непредсказуемом порядке, но s в конце будет снова равен 1.

Обычный семафор может принимать любые неотрицательные целые значения, двоичный семафор – только значения 0 и 1. Следовательно, опера-

Средства разработки параллельных программм

53

ция V() может быть выполнена для двоичного семафора, только если его значение 0:

V(sbin): <await (sbin=0) sbin=sbin+1;>

Семафоры могут быть реализованы как аппаратно, так и программно.

Решение задачи критической секции с помощью семафоров

Семафоры были придуманы отчасти для решения задачи КС, которая в их терминах решается красиво и просто:

sem mutex=1; # семафор: mutex = 1 => КС свободна,

# mutex = 0 => КС занята

process CS [i=1 to n] { while (true) {

P(mutex); # протокол входа

критическая секция; V(mutex); # протокол выхода

некритическая секция;

}

}

Решение задачи об обедающих философах с использованием семафоров

Известная классическая задача об обедающих философах демонстрирует использование семафоров для обеспечения безопасного доступа к пересекающимся ресурсам.

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

При решении задачи вилки можно представить массивом семафоров sem fork[5] (состоянию «i-ая вилка свободна» соответствует fork[i]=1). Для безопасного пользования вилками процесс еды должен быть оформлен критической секцией (философы в нашем алгоритме, как многие выдающиеся люди – левши):

P(fork[i]); P(fork[(i+1)%n]); # взять левую вилку, потом правую поесть;

V(fork[i]); V(fork[(i+1)%n]); # положить левую вилку, потом правую

Средства разработки параллельных программм

54

Однако если все философы будут выполнять этот симметричный код, то поскольку процесс взятия обеих вилок одним философом не является неделимой операцией, то может возникнуть взаимная блокировка. Тупиковая ситуация будет спровацирована, если каждый философ возьмет по одной вилке (в данном случае левой) и будет бесконечно удерживать их, не имея возможности взять правую вилку, поскольку она заблокирована соседом. Одним из возможных решений без взаимной блокировки является появление потока-«правши», который берет сначала правую вилку, а потом левую.

sem fork[5] = {1, 1, 1, 1, 1};

process Left_Handed_Philosopher[i = 0 to 3] { while (true) {

P(fork[i]); P(fork[i+1]); # взять левую вилку, потом правую

поесть;

V(fork[i]); V(fork[i+1]); # положить левую вилку, потом правую

поразмыслить;

}

}

process Right_Handed_Philosopher[4] { while (true) {

P(fork[0]); P(fork[4]); # взять правую вилку, потом левую

поесть;

V(fork[0]); V(fork[4]); # положить левую вилку, потом правую

поразмыслить;

}

}

Возможна реализация и симметричного решения задачи о философах.

Реализация барьеров с помощью семафоров

Применение семафоров естественно не ограничивается только задачей о КС. Так же легко с помощью семафоров реализовать барьерную синхронизацию: для каждого потока описывается сигнализирующий семафор s (с нулевым начальным значением). Тогда каждый поток сигнализирует о своем прибытии к барьеру, выполняя операцию V() для своего семафора, а затем ожидает прибытия к барьеру другого потока, выполняя для его семафора операцию P().

Решение задачи барьерной синхронизации с помощью семафоров с управляющим потоком выглядит следующим образом:

sem arrive[1:n]=([n] 0), continue[1:n]=([n] 0);

process Woker [i=1 to n] { while (true) {

код решения задачи i;

V(arrive[i]); P(continue[i]);

}

}

Средства разработки параллельных программм

55

process Coordinator {

 

while (true) {

 

 

for [i=1 to n]

P(arrive[i]);

 

for [i=1 to n]

V(continue[i]);

 

}

 

 

}

 

 

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

Условная синхронизация с помощью семафоров

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

Задача. Потоки производитель и потребитель разделяют буфер, состоящий из n ячеек. Производитель передает сообщение потребителю, помещая его в конец очереди буфера. Потребитель сообщение извлекает из начала очереди буфера.

Рассмотрим сначала решение для случая одного производителя и одного потребителя. Представим буфер массивом buf[n], n>1 (можно использовать связанный список). Пусть front – индекс первого сообщения в очереди; rear – индекс первой пустой ячейки после сообщения в конце очереди. Тогда, выполняя последовательность операторов

buf[rear] = data; rear = (rear+1) % n

производитель помещает сообщение в буфер и, выполняя последовательность операторов

result = buf[front]; front = (front+1) % n

потребитель извлекает сообщение из буфера.

В этом случае синхронизация по условию нужна для того, что бы сообщение нельзя было забрать из пустой очереди и поместить в полный буфер. Семафоры используются аналогично флагам. Прежде чем поместить сообщение в буфер производитель дожидается освобождения хотя бы одной ячейки, что можно реализовать n-местным семафором empty с начальным значением n. Поток-потребитель, забирая значение из буфера, каждый раз увеличивает значение семафора empty на единицу, выполняя над ним операцию V(empty) (сигнализируя, что произошло событие «опустошить буфер»). Потокпроизводитель перед тем, как положить значение выполняет для семафора empty операцию P(empty), убеждаясь в наличие в буфере свободных ячеек. Для обеспечения безопасного изъятия данных из буфера требуется второй n- местный семафор full с начальным значением 0. Для него потокпроизводитель выполняет операцию V(full) при каждом пополнении буфера, а

Средства разработки параллельных программм

56

процесс-потребитель операцией P(full) убеждается в возможности безопасного извлечения данных.

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

typeT buf[n]; # массив некоторого типа T int front = 0, rear = 0;

sem empty = n, full = 0; # n-2 <= empty + full <= n

process Produser { while (true) {

создать сообщение data;

P(empty); buf[rear] = data; rear = (rear+1) % n; V(full); # поместить data в буфер

}

}

process Consumer { while (true) {

P(full); result = buf[front]; front = (front+1) % n; V(empty); # извлечь сообщение result

потребить result

}

}

В случае наличия нескольких производителей и нескольких потребителе необходимо исключать ситуацию, когда несколько процессов помещают сообщение в одну ячейку буфера или извлекают сообщение из одной и той же ячейки. Для этого следует завести еще два семафора для обеспечения критических секций отдельно для производителя, отдельно для потребителя. В нашем примере это семафоры mutexD («сейчас что-то положу и мне не мешать») и mutexF («сейчас что-то извлеку и мне не мешать»). Получается, что пара семафоров empty и full используется для условной синхронизации, а пара семафоров mutexD и mutexF – для взаимного исключения.

typeT buf[n];

# массив некоторого типа Т

int front = 0, rear = 0;

 

sem empty = n, full = 0;

# для условной синхронизации, n-2 <= empty+full <= n

sem mutexD = 1, mutexF = 1;

# для взаимного исключения

process Producer[i = 1 to M] { while (true) {

создать сообщение data;

# поместить data в буфер

P(empty);

P(mutexD);

buf[rear] = data; rear = (rear+1) % n; V(mutexD);

V(full);

}

}

process Consumer[j = 1 to N] { while (true) {

Средства разработки параллельных программм

57

# извлечь и потребить сообщение result P(full);

P(mutexF);

result = buf[front]; front = (front+1) % n; V(mutexF);

V(empty);

потребить result

}

}

Таким образом, семафоры – это уникальный низкоуровневый механизм, который можно использовать для синхронизации потоков (как в случае взаимного исключения, так и в случае условного ожидания). Однако конкретная языковая реализация семафоров всегда имеет ряд особенностей. Более того, как правило, существует несколько взаимно дополняющих друг друга механизмов синхронизации. Например, в библиотеке Pthread синхронизацию потоков можно осуществить в той или иной степени с помощью целого набора объектов – мьютексов, семафоров, различных блокировок, барьеров. Подробно эти механизмы описаны в главе 3. Для синхронизации потоков WinAPI предоставляет большое разнообразие объектов ядра – критические секции, события, семафоры и т.д. В главе 4 обсуждаются возможности синхронизации потоков в Windows и отличие этих механизмов от аналогов библиотеки Pthread.

МОНИТОРЫ

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

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

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

ется неявно, два потока не могут одновременно выполняться в одном мониторе, поскольку не могут одновременно выполняться две процедуры монитора.

Средства разработки параллельных программм

58

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

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

monitor mname {

объявление постоянных переменных операторы инициализации процедуры

}

Процедуры реализуют видимые операции, постоянные переменные разделяются всеми процедурами тела монитора, они существуют, пока существует монитор. Постоянные переменные доступны потокам только через процедуры монитора. Вызов процедуры имеет вид:

call monitor_name.operation_name (arguments)

Операторы внутри монитора не могут обращаться к переменным, объявленным вне монитора. Постоянные переменные инициализируются до вызова его процедур.

Взаимное исключение

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

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

Условные переменные

Условная переменная – переменная нового типа данных, которые используются только внутри монитора.

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

Условная переменная объявляется следующим образом:

empty(cv)
wait(cv) wait(cv, rank) signal(cv) signal_all(cv)
minrank(cv)

Средства разработки параллельных программм

59

cond cv;

Можно объявить массив условных переменных.

Программист не может напрямую обращаться к условной переменной. С ней допускаются следующие операции:

возвращает «истина», если очередь переменной пуста, иначе – «ложь»; ждать в конце очереди;

ждать в порядке возрастания значения ранга (rank); запустить поток из начала очереди и продолжить; запустить все потокы очереди и продолжить; возвращает значение ранга потока в начале очереди ожидания.

Выполнение оператора wait заставляет работающий поток задержаться в конце очереди переменной cv. Чтобы другой поток мог войти в монитор для запуска приостановленного потока, выполнение wait() отбирает у потока вызвавшего ее, исключительный доступ к монитору. Запуск потока, заблокированного wait() на условных переменных, происходит вызовом оператора signal. Если очередь условной переменной пуста, то никакие действия не происходят, иначе запускается поток в начале очереди.

Синхронизация в мониторе

Однако существует один скользкий момент: выполняя операцию signal поток, работая в мониторе, запускает, вообще говоря, другой поток. Возникает дилемма, приводящая к двум возможным вариантам развития событий:

сигнализировать и продолжать (SC): т.е. в мониторе остается вы-

полняющийся поток, а поток, получивший сигнал опять откладывается и получает право выполняться при первом удобном случае;

сигнализировать и ожидать (SW): здесь управление получает поток из очереди, а сигнализатор переходит в очередь (возможно в ее начало – сигнализировать и срочно ожидать).

Таким образом, поток, вызывающий процедуру монитора помещается во входную очередь (если очередь пуста, то поток выполняется сразу). Монитор может освободиться двумя способами: 1) поток, блокирующий монитор, закончил свою работу в мониторе; 2) выполнена операция wait(cv). В любом случае начинает работать поток первый из входной очереди. Если же работающий поток выполняет процедуру signal(cv), то при порядке работы SC поток из начала очереди условной переменной cv перемещается в начало очереди монитора, а в мониторе остается сигнализировавший поток. Если же принят порядок работы SW, то поток, работавший в мониторе, переходит во входную очередь (возможно в начало), а запускается поток, первый в очереди

Средства разработки параллельных программм

60

на условной переменной. Схема синхронизации в мониторах изображена на рис. 2.6.

 

 

 

 

Очередь

 

 

 

 

 

 

 

 

переменной

 

 

Ожидание

 

 

 

SC

 

 

 

условия

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Вход в

 

 

 

 

 

SW

 

монитор

 

 

 

 

 

 

 

 

 

Входная

 

Монитор

 

 

 

Выполнение

 

 

 

 

 

 

 

свободен

 

 

 

в мониторе

 

очередь

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Возврат

SW

Рис. 2.6. Синхронизация в мониторе

В силу ряда причин процедура «сигнализировать и продолжать» предпочтительнее. Первой была предложена процедура «сигнализировать и ожидать», но и в ОС Unix, и в библиотеках языков программирования C и Java принят именно порядок SC.

Условная синхронизация с помощью мониторов

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

Задача. Потоки производитель и потребитель разделяют буфер, состоящий из n ячеек. Производитель передает сообщение потребителю, помещая его в конец очереди буфера. Потребитель сообщение извлекает из начала очереди буфера.

Пусть по-прежнему буфер представлен массивом buf[n], n>1; front – индекс первого сообщения в очереди; rear – индекс первой пустой ячейки после сообщения в конце очереди; count – количество сообщений в буфере.

Рассмотрим монитор с двумя операциями: deposit – «положить в буфер» и fetch – «извлечь из буфера». Для реализации условного ожидания вместо семафоров empty и full используем две условные переменные: not_empty получает сигнал, когда count > 0, т.е. из буфера можно извлекать данные; not_full получает сигнал, когда count < n, т.е. в буфер можно положить данные. Задержка на условных переменных происходит в цикле while.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]