Распределение ресурсов и планирование
Распределение ресурсов — это задача решения, когда процесс может получить доступ к ресурсу. В параллельных программах ресурсом является все то, при попытке получения чего работа процесса может быть приостановлена. Сюда включается вход в критическую секцию, доступ к базе данных, ячейка кольцевого буфера, область памяти, использование принтера и тому подобное. Несколько частных случаев задачи о распределении ресурсов были рассмотрены выше. В большинстве использовалась простейшая стратегия распределения ресурсов: если некоторый процесс ждет и ресурс свободен, то ресурс распределяется. Например, в решении задачи критической секции (раздел 4.2) разрешение на вход дается какому-нибудь из ожидающих процессов; попытка определить, какой именно процесс получит разрешение на вход, не делается. Так же и в задаче о кольцевом буфере (раздел 4.2) не было попытки контролировать порядок получения доступа производителей и потребителей к буферу. Лишь в задаче о читателях и писателях рассматривалась более сложная стратегия планирования. Но там целью было дать преимущество классу процессов, а не отдельным процессам.
В данном разделе показано, как реализовать общие стратегии распределения ресурсов и, в частности, как явно управлять выбором процесса, получающего ресурс, когда этого ожидают несколько процессов. Сначала описывается общая структура решения, затем реализация одной из стратегий распределения ресурсов — "кратчайшее задание". В решении использован метод передачи эстафеты и представлена идея частных семафоров, на основе которых решаются другие задачи о распределении ресурсов.
150 Часть 1. Программирование с разделяемыми переменными
4.5.1. Постановка задачи и общая схема решения
В любой задаче распределения ресурсов процессы конкурируют за использование элементов разделяемого ресурса. Процесс запрашивает один или несколько элементов, выполняя операцию request, часто реализованную в виде процедуры.
Параметры операции request указывают необходимое количество элементов ресурса, определяют особые характеристики (например, размер блока памяти) и идентифицируют запрашивающий процесс. Каждый элемент разделяемого ресурса либо свободен, либо занят (используется). Запрос может быть удовлетворен, только когда все необходимые элементы свободны. Таким образом, процедура request ожидает освобождения достаточного количества элементов ресурса, а затем возвращает запрошенное число элементов. После использования элементов ресурса процесс освобождает их операцией release. Параметры операции release задают идентификаторы возвращаемых элементов. Процесс может освобождать элементы в порядке и количествах, отличных от тех, в которых они запрашивались.
Если опустить представление элементов ресурса, то общая схема операций request и release такова.
Операции должны быть неделимыми, поскольку каждой из них необходим доступ к представлению элементов ресурса. Поскольку в этом представлении используются переменные, отличающиеся от других переменных программы, операции будут неделимыми по отношению к другим действиям и, следовательно, могут выполняться параллельно с ними.
Эту общую схему решения можно реализовать с помощью метода передачи эстафеты (раздел 4.4). Операция request имеет вид обычного оператора await, поэтому реализуется следующим фрагментом программы.
Операция release тоже имеет вид простого неделимого действия и реализуется таким фрагментом программы.
Как и раньше, семафор е управляет входом в критическую секцию, а фрагмент кода SIGNAL запускает на выполнение приостановленные процессы (если ожидающий запрос может быть удовлетворен) или снимает блокировку семафора входа с помощью операции V (е). Код DELAY в операции request аналогичен фрагментам кода в начале протоколов входа процессов читателей и писателей (см. листинги 4.11 и 4.12). Он запоминает, что появился новый запрос, который должен быть приостановлен, снимает блокировку с семафора входа с помощью операции V (е) и блокирует запрашивающий процесс на семафоре задержки.
Детали реализации кода SIGNAL в каждой конкретной задаче распределения ресурсов зависят от условий задержки и их представления. В любом случае код DELAУ должен сохранять параметры, характеризующие приостановленный запрос для дальнейшего их использования кодом SIGNAL. Кроме того, для каждого условия задержки нужен один семафор условия.
Глава 4. Семафоры 151
В следующем разделе разработано решение частной задачи распределения ресурсов, которое может служить основой для решения любой такой задачи. В упражнениях приводятся некоторые дополнительные задачи распределения ресурсов.
4.5.2. Распределение ресурсов по схеме "кратчайшее задание"
"Кратчайшее задание" (КЗ, Shortest-Job-Next — SJN) — это стратегия распределения ресурсов, которая встречается во многих разновидностях и используется для разных типов ресурсов. Предположим, что разделяемый ресурс состоит из одного элемента (общий случай нескольких элементов рассмотрен в конце данного раздела). Тогда стратегия КЗ определяется следующим образом.
(4.3) Распределение ресурсов по схеме "кратчайшее задание". Несколько процессов соперничают в использовании одного разделяемого ресурса. Процесс запрашивает ресурс, выполняя операцию request (time, id), где целочисленный параметр time определяет длительность использования ресурса этим процессом, а целое число id идентифицирует запрашивающий процесс. Если в момент выполнения операции request ресурс свободен, он выделяется для процесса немедленно, иначе процесс приостанавливается. После использования ресурса процесс освобождает его, выполняя операцию release. Освобожденный ресурс распределяется приостановленному процессу (если такой есть) с наименьшим значением параметра time. Если у нескольких процессов значения time равны, то ресурс отдается тому из них, кто дольше всех ждал.
Стратегию КЗ можно использовать, например, для распределения процессоров (параметр time будет означать время выполнения), для вывода файлов на принтер (time — время печати) или для обслуживания удаленной передачи файлов по протоколу ftp (time — предполагаемое время передачи файла).
Стратегия КЗ привлекательна, поскольку минимизирует общие затраты времени на выполнение задачи. Вместе с тем, ей присуще несправедливое планирование: процесс может быть приостановлен навсегда, если существует непрерывный поток запросов с меньшим временем использования ресурса. (Такая несправедливость крайне маловероятна на практике, если только ресурс не перегружен.) Если несправедливость нежелательна, то можно слегка изменить стратегию КЗ, чтобы предпочтение отдавалось процессам, ожидающим слишком долго. Этот метод называется выдержкой, или старением (aging).
Если процесс выполняет запрос и ресурс свободен, то этот запрос может быть удовлетворен немедленно, поскольку других ожидающих запросов нет. Таким образом, стратегия КЗ "вступает в игру", только если есть несколько ожидающих запросов. Ресурс один, поэтому для хранения сведений о его доступности достаточно одной переменной. Пусть free — логическая переменная, которая истинна, когда ресурс доступен, и ложна, когда он занят. Для реализации стратегии КЗ нужно запоминать и упорядочивать ожидающие запросы. Пусть pairs — набор записей вида (time, id), упорядоченных по значениям поля time. Если две записи имеют одинаковые значения поля time, то они остаются во множестве pairs в порядке их появления. В соответствии с этим определением следующий предикат должен быть глобальным инвариантом. SJN: (pairs — упорядоченный набор) л (free => (pairs == 0) )
Расшифруем: набор pairs упорядочен, а если ресурс свободен, то pairs — пустое множество. Вначале free истинна, а множество pairs пусто, так что предикат SJN выполняется.
Без учета стратегии КЗ запрос может быть удовлетворен, как только ресурс станет доступным. Отсюда получим следующее крупномодульное решение.
152 Часть 1. Программирование с разделяемыми переменными
Однако при использовании стратегии КЗ процесс, выполнивший запрос request, должен быть приостановлен до момента, когда будет свободен ресурс и запрос процесса станет следующим с точки зрения стратегии КЗ.
Из второй части предиката SJN следует, что если переменная free истинна во время выполнения процессом операции request, то множество pairs пусто. Следовательно, приведенного выше условия достаточно для определения, может ли запрос удовлетворяться немедленно. Параметр time играет роль, только если запрос должен быть отложен — т.е. если переменная free ложна. На основе этих соображений можно реализовать операцию request таким образом.
В операции request предполагается, что операции Р над семафором входа е обслуживаются в порядке их появления, т.е. по правилу "первым пришел— первым обслужен". Если этого нет, то порядок обработки запросов может не соответствовать стратегии КЗ.
Осталось воплотить стратегию распределения ресурсов КЗ. Для этого используем множество pairs и семафоры, реализующие фрагменты кода SIGNAL и DELAY. Если запрос не может быть удовлетворен, его следует сохранить, чтобы к нему можно было вернуться после освобождения ресурса. Таким образом, во фрагменте кода DELA Yпроцесс должен:
• вставить параметры запроса в набор pairs;
• освободить управление критической секцией, выполнив операцию V (е);
• остановиться на семафоре до удовлетворения запроса.
Если после освобождения ресурса множество pairs не пусто, то в соответствии со стратегией КЗ только один процесс должен получить ресурс. Таким образом, если есть приостановленный процесс, который теперь может продолжить работу, он должен получить сигнал с помощью операции V для семафора задержки.
В приведенных выше примерах условий задержки было немного, поэтому нужно было всего несколько семафоров условия. Например, в решении задачи о читателях и писателях в конце предыдущего раздела было только два условия задержки. Но здесь у каждого процесса есть свое условие задержки, которое зависит от его позиции в наборе pairs: первый в pairs процесс должен быть запущен перед вторым и так далее. Таким образом, каждый процесс должен ожидать на своем семафоре задержки.
Предположим, что ресурс используют п процессов. Пусть b[n] — массив семафоров, ка ждый элемент которого имеет начальное значение 0. Будем считать, что значения идентификаторов процессов id уникальны и находятся в пределах от 0 до п-1. Тогда процесс id приостанавливается на семафоре b[id]. Дополнив операции request и release соответствующей обработкой множества pairs и массива Ь, получим решение задачи распределения ресурсов по схеме КЗ (листинг4.13).
В листинге 4.13 предполагается, что операция вставки помещает пару параметров в такое место последовательности pairs, которое сохраняет истинность первой части предиката SJN. Следовательно, предикат SJN действительно является инвариантом вне операций request и release, т.е. он является истинным непосредственно после каждой операции Р(е) и перед каждой V (е). Если есть ожидающие запросы, т.е. множество pairs не пусто, оператор if кода выработки сигнала в операции release запускает только один процесс
Глава 4. Семафоры 153
"Эстафетная палочка" переходит к этому процессу, а он присваивает переменной free значение ложь. Этим гарантируется истинность второй части предиката SJN, когда множество pairs не пусто. Поскольку ресурс один, остальные запросы не могут быть удовлетворены, так что сигнальный код операции regues t состоит из одной операции V (е).
Элементы массива семафоров b в листинге 4.13 являются примером так называемых частных семафоров.
(4.4) Частный семафор. Семафор s называется частным, если операцию Р над ним выполняет только один процесс.
Когда процесс в листинге 4.13 должен быть приостановлен, он выполняет операцию Р (b [ id]) для блокировки на собственном элементе массива Ь.
Частные семафоры полезны в ситуациях, когда необходимо иметь возможность сигнализировать отдельному процессу. В некоторых задачах распределения ресурсов, однако, условий задержки может быть меньше, чем процессов, претендующих на использование ресурса.
Тогда эффективнее использовать один семафор для каждого условия, а не для каждого процесса. Например, если память выделяется блоками нескольких заданных размеров, и не важен порядок распределения блоков процессам, конкурирующим за блоки одного размера, то достаточно использовать по одному семафору задержки для каждого возможного размера блока.
Решение в листинге 4.13 легко обобщить для использования ресурсов, состоящих из нескольких элементов. В этой ситуации каждый элемент может быть свободен или занят, а операции request и release должны использовать параметр amount, определяющий требуемое количество элементов ресурса. Изменим решение в листинге 4.13 следующим образом:
• булеву переменную free заменим целочисленной переменной avail для хранения количества доступных элементов;
• в операции request проверим условие amount <= avail. Если оно истинно, выделим amount элементов ресурса. Если нет, запомним, сколько элементов ресурса запрошены перед приостановкой процесса;
154 Часть 1 Программирование с разделяемыми переменными
• в операции release увеличим значение avail на amount, после чего определим, можно ли удовлетворить запрос самого старого из отложенных процессов с минимальным значением параметра time. Если да — запустим его. Если нет, выполним V (е).
После освобождения элементов ресурса возможно удовлетворение нескольких ожидающих запросов. Например, может быть два приостановленных процесса, которым в сумме нужно не больше элементов, чем было освобождено. Тогда первый процесс после получения необходимого ему количества элементов должен выработать сигнал для второго процесса. Таким образом, протокол выработки сигнала в конце операции request должен быть таким же, как и протокол в конце операции release.