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

Sb97573

.pdf
Скачиваний:
4
Добавлен:
13.02.2021
Размер:
685.79 Кб
Скачать

МИНОБРНАУКИ РОССИИ

__________________________

Санкт-Петербургский государственный электротехнический университет «ЛЭТИ» им. В. И. Ульянова (Ленина)

________________________________________________________

В. В. СИДЕЛЬНИКОВ В. В. ШИРОКОВ

СРЕДСТВА СИНХРОНИЗАЦИИ МНОГОЗАДАЧНЫХ ПРИЛОЖЕНИЙ. МОНИТОР ХОАРА

Учебно-методическое пособие

Санкт-Петербург Издательство СПбГЭТУ «ЛЭТИ»

2019

1

УДК 004.451(07)

ББК З973-018я7

С34

Сидельников В. В., Широков В. В.

С34 Средства синхронизации многозадачных приложений. Монитор Хоара: учеб.-метод. пособие. СПб.: Изд-во СПбГЭТУ «ЛЭТИ», 2019. 44 с.

ISBN 978-5-7629-2412-2

Описываются средства синхронизации многозадачных приложений в виде, предложенном в работах Дейкстры и Хоара. Особое внимание уделяется монитору, детально рассматриваются способы передачи управления при работе с условной переменной, определяемые как «семантика Хоара» и «семантика Mesa».

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

Предназначено для подготовки бакалавров по направлению 09.03.02 «Информационные системы и технологии» и специалистов по направлению 090301.65 «Компьютерная безопасность».

УДК 004.451(07)

ББК З973-018я7

Рецензент канд. техн. наук Ф. Р. Гальяно Сизаско (АО СПИИРАН НТБВТ).

Утверждено редакционно-издательским советом университета

в качестве учебно-методического пособия

ISBN 978-5-7629-2412-2

© СПбГЭТУ «ЛЭТИ», 2019

2

1. МНОГОЗАДАЧНОСТЬ И СИНХРОНИЗАЦИЯ

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

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

При таком параллелизме программа, написанная на некотором языке программирования и скомпилированная в машинное представление, обрастает атрибутами, необходимыми операционной системе для управления ее выполнением, и термин «программа» уже не вполне соответствует той сущности, с которой операционная система имеет дело.

Чаще всего для подобного определения используются термины «процесс» или «поток», которые происходят из представлений UNIXподобных операционных систем, определяют конструкции, существенно отличающиеся друг от друга, и отражают специфику реализации. Здесь же в дальнейшем хотелось бы использовать термин, который, с одной стороны, корректно отражал бы положение вещей, а с другой – был бы достаточно общим. Не вдаваясь далее в анализ терминологических особенностей и для избежания путаницы в определениях, там, где не затрагиваются особенности реализации, будем использовать термин задача, а под многозадачностью будем понимать возможность выполнения нескольких задач параллельно во времени.

При любом способе реализации многозадачности необходимо наличие механизмов, обеспечивающих согласование выполнения задач во времени, их

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

3

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

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

Далее будут рассмотрены механизмы синхронизации в том виде, как они были предложены их разработчиками, а затем так, как они реализуются в современных операционных системах.

В ряде примеров, иллюстрирующих многозадачное выполнение, будем использовать запись следующего вида:

Parbegin P1; P2; . . . Pn Parend,

где подразумевается, что действия Pi, заключенные в «операторные скобки» Parbegin ... Parend, выполняются параллельно во времени до тех пор, пока не завершится самое длительное из них.

2. СЕМАФОРЫ И МЬЮТЕКСЫ

Наиболее распространенной проблемой синхронизации является вопрос обеспечения взаимного исключения. Если несколько задач используют некоторый разделяемый ими ресурс (например, некоторое данное), то в каждый момент времени доступ к этому ресурсу должна иметь только одна из них. Обычно участок кода, обеспечивающий доступ к разделяемому ресурсу, называют критической секцией или критическим участком. В свое время предлагалось большое количество алгоритмов, позволяющих решить задачу взаимного исключения. Однако естественным пожеланием было иметь некий общий механизм, который можно было использовать при разработке многозадачных приложений. В 1968 г. голландским ученым Эдсгером Дейкстрой была опубликована работа «Взаимодействующие последовательные процессы», в которой он предложил механизм семафоров, предполагающий наличие переменной S – двоичного семафора, способной

4

принимать только 2 значения – «0» или «1». Над такой переменной (семафором) допускались две операции – P(S) и V(S):

P(S):

if (S = 0) then wait(S = 1); S := 0;

V(S):

S := 1;

Задача, которая выполняет операцию P(S) при значении S = 1, «захватывает» семафор и выполняется дальше, установив S в «0». Таким образом, другая задача, выполняющая P(S), будет приостановлена, т. е. будет

блокироваться действием wait и ставиться в очередь заблокированных задач,

связанную с данным семафором. Такая очередь всегда будет создаваться при создании семафора. Заблокированная задача будет находиться в очереди до тех пор, пока некоторая другая задача не выполнит операцию V(S). Операция V(S), устанавливая S := 1, выбирает из очереди первую заблокированную задачу и обеспечивает продолжение ее выполнения с действия, следующего за операцией wait. Дисциплиной обслуживания очереди заблокированных задач в таком случае является FIFO.

Важным условием относительно P- и V-операций является то, что они должны быть неделимыми, т. е. одновременное их выполнение более чем одной задачей невозможно.

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

Нетрудно убедиться, что представленный механизм позволяет решить задачу взаимного исключения. Пусть имеется приложение, cостоящее из n задач Ri, каждая из который включает в себя критическую секцию CSi, код которой работает с некоторым ресурсом, разделяемым всеми задачами. Если заключать CSi в «операторные скобки» P(S)–V(S), как показано ниже, то взаимное исключение будет обеспечено.

S : ДвоичныйСемафор;

Ri : loop

Начало_I;

5

P(S);

CSi;

V(S);

Остальное_I; end loop;

Parbegin R1; . . . Rn; Parend.

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

Развитием двоичного семафора является счетный семафор. Семафор S

может принимать целые значения из диапазона 0...n. Соответствующие P- и V-операции определяются так:

S : СчетныйСемафор(m,n); P(S):

if (S = 0) then wait(S > 0); S := S - 1;

V(S):

if (S < n) then S := S + 1;

При описании счетного семафора следует указывать его начальное m и максимальное n значения (n m ≥ 0). Если S > 0, то каждый вызов P(S) уменьшает значение S на единицу, при этом вызвавшая P(S) задача продолжает работать. При S = 0 все последующие вызовы P(S) будут блокировать вызывающую задачу и ставить ее в очередь.

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

6

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

Для иллюстрации механизма семафоров рассмотрим хорошо известный пример, который часто называют «Поставщик – Потребитель».

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

1.«Поставщик» не может записывать информацию и блокируется, если буфер полон.

2.«Потребитель», пытаясь читать информацию, блокируется, если буфер пуст.

3.Не допускается одновременная запись и чтение информации.

Одно из решений данного примера строится на использовании трех семафоров – одного двоичного и двух счетных:

Доступ : ДвоичныйСемафор; Не_пуст : СчетныйСемафор(n,n); Не_полон : СчетныйСемафор(0, n); Поставщик:

loop

Производство_Информации;

P(Не_полон);

// (*)

P(Доступ);

// (**)

Запись_в_буфер;

 

V(Доступ);

 

V(Не_пуст); Остальное_Поставщика;

end loop;

Потребитель: loop

P(Не_пуст);

P(Доступ); Запись_в_буфер; V(Доступ); V(Не_полон);

7

Остальное_Потребителя; end loop;

Parbegin Поставщик; Потребитель; Parend.

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

Менее очевидным, но не менее опасным источником неприятностей в рассмотренном примере является возможность освобождения счетного семафора (вызов V(Не_пуст)) той задачей, которая его не «захватывала» (не выполняла P(Не_пуст)). Таким образом, при использовании механизма семафоров следует придерживаться определенных рекомендаций структурирования кода, стараться, например, чтобы освобождение семафора (V-операция) выполнялось той же задачей, что и его захват (P-операция).

Очевидно, что при «аккуратном» использовании счетного семафора и строгом слежении за последовательностью выполнения P- и V-операций можно обходиться без двоичного семафора. При необходимости надо просто определять счетный семафор с начальным значением «0» или «1» и максимальным «1». Другими словами, двоичный семафор можно считать частным случаем счетного семафора.

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

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

M : mutex;

8

lock:

if (M = занят) then wait(M = свободен); M := занят;

Owner := текущая_задача; unlock:

M := свободен;

Owner := NULL;

На первый взгляд представляется, что мьютекс является аналогом двоичного семафора, lock и unlock являются неделимыми и функционально подобны P- и V-операциям. Отличительной особенностью является наличие у мьютекса атрибута владелец – Owner. «Владельцем» становится текущая задача, в коде которой выполнен lock над «свободным» мьютексом, и в этом случае говорят, что задача захватила этот мьютекс. Любая другая задача, пытающаяся захватить этот объект, будет заблокирована и поставлена в очередь. Выполнить действие unloсk и освободить мьютекс может только его владелец, попытка выполнить unlock другой задачей приведет к ошибке. Атрибут Owner не требует от пользователя его инициализации, в исходном состоянии его значение NULL, а далее устанавливается и «сбрасывается» автоматически при захвате и освобождении мьютекса; другими словами, программист может и не замечать его существования. При этом наличие такого атрибута позволяет решать сложные проблемы, связанные с многозадачностью, как, например, проблему инверсии приоритетов, рассмотрение которой выходит за рамки представленного материала. Таким образом, считать мьютекс аналогом двоичного семафора было бы неправильно.

3. МОНИТОР ХОАРА

Семафоры и мьютексы, предоставляя большую свободу в их использовании, часто провоцируют на построение сложных последовательностей захвата и освобождения, что при неаккуратном написании кода зачастую приводит к довольно запутанным программным конструкциям и подчас неразрешимым проблемам при отладке даже не слишком сложных задач. В 1974 г. английский профессор Чарльз Энтони Хоар опубликовал статью, в которой изложил идею монитора – конструкции, позволяющей инкапсулировать процедуры и данные, обеспечивая к ним взаимно исключающий доступ задач:

9

monitor имя_монитора;

декларация данных, локальных для монитора; procedure имя_процедуры (формальные параметры); begin тело процедуры end;

Декларация других процедур; begin

Инициализация локальных данных;

еnd.

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

В качестве примера обратимся снова к задаче «Поставщик – Потребитель».

monitor Буфер;

СамБуфер array[1..ДлинаБуфера] : Данное; СчетчикЗаписей : integer;

procedure Записать(d : Данное); begin

...

end;

function Прочитать : Данное; begin

...

end; begin

СчетчикЗаписей := 0; End Буфер;

Поставщик: loop

D := Производство;

Буфер.Записать(D); end loop;

Потребитель:

10

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