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

Sb97573

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

pthread_mutex_lock(&mutex);

// 2

. . .

 

pthread_mutex_unlock(&mutex);

// 3

. . .

 

pthread_mutex_unlock(&mutex);

// 4

. . .

 

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

int pthread_mutexattr_setrecurcive(pthread_mutexattr_t* attr, int recursive); int pthread_mutexattr_getrecurcive(pthread_mutexattr_t* attr, int recursive);

где recursive:

PTHREAD_RECURSIVE_ENABLE – разрешить рекурсивный захват; PTHREAD_RECURSIVE_DISABLE – (по умолчанию) запретить.

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

Условная переменная

Следующая функция инициализирует условную переменную: int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr),

где cond – указатель на описатель условной переменной; attr – указатель на атрибуты условной переменной.

Следующая функция приводит к ожиданию выполнения условия: int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex). Поток, в

котором вызвана эта функция, ставится в очередь, связанную с переменной cond (cond – указатель на описатель условной переменной; mutex – указатель на описатель мьютекса).

Следующая функция посылает сигнал о выполнении условия,

связанного с переменной cond: int pthread_cond_signal( pthread_cond_t* cond)

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

31

Следующая функция посылает сигнал о выполнении условия, связанного с переменной cond, всем потокам, заблокированным на этой переменной: int pthread_cond_broadcast( pthread_cond_t* cond ); все потоки,

заблокированные на переменной cond, переводятся в очередь потоков, готовых к выполнению.

В качестве примера приведем возможную реализацию функций записи и чтения в задаче «Поставщик – Потребитель»:

pthread_mutex_t mutexFull, mutexEmpty; pthread_cond_t condFull, condEmpty; int buffer_full = 0, buffer_empty = 1;

/* функция записи в буфер, вызываемая «Поставщиком» */ pthread_mutex_lock(&mutexFull);

while (buffer_full)

{

pthread_cond_wait(&condFull, &mutexFull);

}

/* Здесь подразумевается код, реализующий запись в буфер и установку значения переменной buffer_empty*/ pthread_cond_signal(&condEmpty); pthread_mutex_unlock(&mutexFull);

/* функция чтения из буфера, вызываемая «Потребителем»

*/ pthread_mutex_lock(&mutexEmpty); while (buffer_empty)

{

pthread_cond_wait(&condEmpty, &mutexEmpty);

}

/* Здесь подразумевается код, реализующий чтение из буфера и установку значения переменной buffer_full */ pthread_cond_signal(&condFull); pthread_mutex_unlock(&mutexEmpty);

32

7. ПРИМЕРЫ ЗАДАЧ СИНХРОНИЗАЦИИ И ИХ РЕАЛИЗАЦИЯ

Задача об обедающих философах

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

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

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

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

33

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

Модель поведения i-го философа можно представить следующим образом:

while (true) {

Думает(i); Захват_вилок(i); Обедает(i); Освобождение_вилок(i);

}

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

int const Nphil = 5.

При этом каждая вилка является общим ресурсом для двух соседних философов-потоков. В этих условиях задача сводится к реализации функции Захват_вилок(int philnum) и Освобождение_вилок(int philnum) (philnum –

номер философа), удовлетворяющей исходной постановке.

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

Функция Захват_вилок(int philnum) может быть реализована следующим образом. Когда философ с номером philnum решил пообедать, он проверяет состояния двух соседних вилок. Если хотя бы одна из них занята, философ переходит в состояние ожидания освобождения этих вилок. Если обе вилки свободны, философ захватывает их и начинает обедать.

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

34

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

Прежде всего, для обеспечения режима взаимного исключения при работе с функциями Захват_вилок(int philnum) и Освобождение_вилок(int philnum) из разных потоков введем мьютекс: pthread_mutex_t mutex.

Для обеспечения ожидания потока-философа в случае занятости нужных вилок введем условные переменные: pthread_cond_t philCV[Nphil].

Далее объявим массив чисел, соответствующих состояниям вилок: int vil[Nphil]; //0 – вилка свободна; 1 – вилка занята

ивведем 4 массива, определяющих по номеру философа номера его левых и правых вилок, а также номера философов слева и справа от него:

int const vl[Nphil] = {0,1,2,3,4};//левая вилка; int const vp[Nphil] = {4,0,1,2,3};//правая вилка;

int const fl[Nphil] = {1,2,3,4,0};//левый философ; int const fp[Nphil] = {4,0,1,2,3};//правый философ.

Теперь текст функции Захват_вилок(int philnum) можно представить следующим образом:

void Захват_вилок(int philnum)

{

pthread_mutex_lock(&mutex); int Lv = vl[philnum];

//определение номера левой вилки по номеру философа int Rv = vp[philnum];

//определение номера правой вилки по номеру философа //если хотя бы одна из вилок занята, ожидание

if ((vil[Lv] == 1)||(vil[Rv] == 1)) { pthread_cond_wait(&philCV[philnum],&mutex);

}

vil[Lv] = 1;//захват вилок vil[Rv] = 1; pthread_mutex_unlock(&mutex);

}

Всоответствии с описанием работы функции Освобождение_вилок(int philnum) код функции будет выглядеть следующим образом:

void Освобождение_вилок(int philnum)

{

35

pthread_mutex_lock(&mutex); int Lv = vl[philnum];

//определение номера левой вилки по номеру философа int Rv = vp[philnum];

//определение номера правой вилки по номеру философа vil[Lv] = 0;//освобождение вилок

vil[Rv] = 0;

int Lp = fl[philnum];

//определение номера левого соседа по номеру философа int Rp = fp[philnum];

//определение номера правого соседа по номеру философа Lv = vl[Lp]; //определение номеров других вилок

Rv = vp[Rp];

//если другая вилка свободна, левый философ может есть if (vil[Lv] == 0) {

pthread_cond_signal(&philCV[Lp]);

}

//если другая вилка свободна, правый философ может есть if (vil[Rv] == 0) {

pthread_cond_signal(&philCV[Rp]);

}

pthread_mutex_unlock(&mutex);

}

Обратим внимание на следующий фрагмент кода функции Захват_вилок(int philnum):

if ((vil[Lv] == 1)||(vil[Rv] == 1)) { pthread_cond_wait(&philCV[philnum],&mutex);

}

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

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

36

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

Выбор процесса для реального выполнения осуществляет ядро ОС.

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

while ((vil[Lv] == 1)||(vil[Rv] == 1)) { pthread_cond_wait(&philCV[philnum],&mutex);

}

Если повторная проверка покажет, что вилки заняты, процесс вернется в состояние ожидания.

Назначение однородных ресурсов

Имеется N задач и M единиц однородных ресурсов. Любая задача может запросить k единиц ресурсов в диапазоне от 1 до M. Если требуемое количество ресурсов имеется в наличии, то они предоставляются задаче и общее число свободных ресурсов уменьшается на k единиц. Если требуемого количества ресурсов нет в наличии, задача блокируется и ждет их освобождения.

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

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

37

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

Модель поведения каждой задачи может быть представлена следующим образом:

while (true) {

Работа без ресурсов(); Запрос_ресурсов(nr); Работа с ресурсами(); Освобождение_ресурсов(nr);

}

Возможность избежать бесконечного ожидания будет зависеть от работы функций Запрос_ресурсов(int nr) и Освобождение_ресурсов(int nr).

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

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

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

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

38

активизации первой задачи количество накопится и задача будет активизирована.

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

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

Общее количество ресурсов и общее количество задач, пользующихся ресурсами, объявим как int const Nres и int const Nproc соответственно, а текущее количество свободных ресурсов объявим как переменную

int nfree = Nres.

Определим очередь задач, ожидающих ресурсы, как вектор vector <TProc> ProcList, элементами которого будут структуры вида typedef struct _TProc

{

int resnum; //количество запрашиваемых ресурсов int procnum;//номер задачи (0 – Nproc-1)

} TProc;

Очевидно, что функции задач Запрос_ресурсов(int nr) и Освобождение_ресурсов(int nr) должны работать в режиме взаимного исключения, для чего введем мьютекс: pthread_mutex_t mutex.

Для обеспечения ожидания потока при отсутствии нужного количества ресурсов введем условные переменные: pthread_cond_t resCV[Nproc].

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

Запрос_ресурсов(int nr, int np); Освобождение_ресурсов(int nr, int np).

С учетом приведенных выше рассуждений текст функции Запрос_ресурсов(int nr, int np) каждого потока можно представить следующим образом:

void Запрос_ресурсов(int nr,int np)

{

pthread_mutex_lock(&mutex);

if ((nr > nfree)||(!ProcList.empty())) {

39

//если свободных ресурсов меньше, чем запрашивается, или //есть очередь задач, ждущих ресурсы

do {

//формируем элемент очереди

TProc proc; proc.resnum = nr; proc.procnum = np;

//и помещаем его в очередь

ProcList.push_back(proc);

//блокируем на условной переменной pthread_cond_wait(&resCV[np],&mutex);

//после активизации делаем повторную проверку

} while (nr > nfree);

}

nfree = nfree - nr; pthread_mutex_unlock(&mutex);

}

Текст функции Освобождение_ресурсов(int nr,int np) можно представить следующим образом:

void Освобождение_ресурсов(int nr, int np)

{

pthread_mutex_lock(&mutex); nfree = nfree + nr;

int tmpnfree = nfree; //просматриваем очередь

while (!ProcList.empty()) { //берем первый элемент

TProc proc = ProcList.front();

//если для первой задачи достаточно ресурсов if (proc.resnum <= tmpnfree) {

//то исключаем его из очереди

ProcList.erase(ProcList.begin());

//и активизируем ожидающую задачу pthread_cond_signal(&resCV[proc.procnum]);

40

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