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