- •Часть 4. Локальное взаимодействие процессов
- •Глава 16. Блокирование записей 89
- •12.2. Процессы, потоки и общий доступ к информации
- •12.3. Живучесть объектов ipc
- •12.4. Пространства имен
- •12.5. Действие команд fork, exec и exit на объекты ipc
- •12.6. Комментарии к примерам ipc
- •12.7. Выводы по главе 12
- •12.8. Упражнения по главе 12
- •Глава 13. Именованные и неименованные каналы
- •13.1. Введение
- •13.2. Приложение типа клиент-сервер
- •13.3. Программные каналы
- •13.4. Функции popen и pclose
- •13.5. Именованные каналы (fifo)
- •13.6. Некоторые свойства именованных и неименованных каналов
- •13.7. Один сервер, несколько клиентов
- •13.8. Последовательные и параллельные серверы
- •13.9. Ограничения программных каналов и fifo
- •13.10. Выводы по главе 13
- •13.11. Упражнения по главе 13
- •Глава 14. Программные потоки
- •14.1. Введение
- •14.2. Концепция потоков
- •14.3. Идентификация потоков
- •14.4. Создание потока
- •14.5. Завершение потока
- •Функции управления процессами и потоками
- •14.6. Установка атрибутов потока
- •14.7. Реентерабельность
- •Альтернативные версии функций, безопасные в многопоточной среде
- •14.8. Локальные данные потоков
- •14.9. Принудительное завершение потоков
- •Некоторые точки выхода, определенные стандартом Posix.1
- •14.10. Потоки и сигналы
- •14.11. Выводы по главе 14
- •14.12. Упражнения по главе 14 Глава 15. Средства синхронизации потоков
- •15.1. Введение
- •15.2. Взаимные исключения: установка и снятие блокировки
- •15.2.1. Схема производитель-потребитель
- •15.2.2. Блокирование и опрос
- •15.2.3. Предотвращение тупиковых ситуаций
- •15.3. Условные переменные
- •15.3.1. Ожидание и сигнализация
- •15.3.2. Исключение состояния гонок
- •15.4. Блокировки чтения-записи
- •15.5. Атрибуты средств синхронизации потоков
- •15.5.1. Атрибуты взаимных исключений
- •Поведение взаимных исключений различных типов
- •15.5.2. Атрибуты условных переменных
- •15.5.3. Атрибуты блокировок чтения-записи
- •15.6. Выводы по главе 15
- •15.7. Упражнения по главе 15
- •Глава 16. Блокирование записей
- •16.1. Введение
- •16.2. Блокирование записей и файлов
- •16.3. Блокирование записей с помощью fcntl по стандарту Posix
- •16.4. Рекомендательная блокировка
- •16.5. Обязательная блокировка
- •16.6. Приоритет чтения и записи Выводы по главе 16
- •Упражнения по главе 16 Глава 17. System V ipc
- •17.1. Введение
- •17.2. Ключи типа key_t и функция ftok
- •17.3. Структура ipc_perm
- •17.4. Создание и открытие каналов ipc
- •17.5. Разрешения ipc
- •17.6. Программы ipcs и ipcrm
- •17.7. Ограничения ядра
- •17.8. Выводы по главе 17
- •17.9. Упражнения по главе 17
- •Глава 18. Очереди сообщений System V
- •18.1. Введение
- •18.2. Функция msgget
- •18.3. Функция msgsnd
- •18.4. Функция msgrcv
- •18.5. Функция msgctl
- •18.6. Пример программы клиент-сервер
- •18.7. Мультиплексирование сообщений
- •18.7.1. Пример: одна очередь на приложение
- •18.7.2. Пример: одна очередь для каждого клиента
- •18.8. Ограничения, накладываемые на очереди сообщений
- •18.9. Выводы по главе 18
- •18.10. Упражнения по главе 18
- •Глава 19. Семафоры System V
- •19.1. Введение
- •19.2. Функция semget
- •19.3. Функция semop
- •19.4. Функция semctl
- •19. . Ограничения семафоров System V
- •19. . Выводы по главе 19
- •19. . Упражнения по главе 19 Глава 20. Введение в разделяемую память
- •20.1. Введение
- •20.2. Функции mmap, munmap и msync
- •20.3. Увеличение счетчика в отображаемом в память файле
- •20.4. Неименованное отображение в память
- •20.5. Обращение к объектам, отображенным в память
- •20.6. Выводы по главе 20
- •20.7. Упражнения по главе 20
- •Глава 21. Разделяемая память System V
- •21.1. Введение
- •21.2. Функция shmget
- •21.3. Функция shmat
- •21.4. Функция shmdt
- •21.5. Функция shmctl
- •21.6. Ограничения, накладываемые на разделяемую память
- •21.7. Выводы по главе 21
- •21.8. Упражнения по главе 21
15.3.1. Ожидание и сигнализация
Для приостановки потоков в ожидании наступления некоторого события предусмотрены две функции. Функция pthread_cond_waitожидает, пока не произойдет указанное событие. Если нужно ограничить время ожидания заданным интервалом времени, то следует использовать функциюpthread_cond_timedwait. При вызове обеих функций мы указываем адрес условной переменной и адрес связанного с ней взаимного исключения.
#include <pthread.h>
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *timeout);
/* обе функции возвращают 0 в случае успеха, код ошибки – в случае неудачи */
Вызывающий поток передает взаимное исключение функции pthread_cond_wait в запертом состоянии, а функция атомарно помещает вызывающий поток в список потоков, ожидающих наступления некоторого события, и отпирает взаимное исключение. Это исключает вероятность того, что событие произойдет между моментом проверки и моментом приостановки потока, благодаря чему поток не пропустит момента наступления ожидаемого события. Когда функция pthread_cond_wait возвратит управление, она снова запрет взаимное исключение.
Функция pthread_cond_timedwait работает аналогичным образом, но дополнительно предоставляет возможность ограничить время ожидания. Значение аргумента timeout определяет, как долго поток будет ожидать наступления события. Время ожидания задается в структуре timespec, в которой время хранится в секундах и наносекундах:
struct timespec
{
time_t tv_sec; /* секунды */
long tv_nsec; /* наносекунды */
};
При использовании этой структуры следует указывать абсолютное время (которое в Unix-подобных ОС отсчитывается от полночи 1 января 1970 года), а не относительное. Это отличает функцию от select, pselect и poll (часть 5), которые в качестве аргумента принимают некоторое количество долей секунды, спустя которое должен произойти возврат. Преимущество использования абсолютного времени заключается в том, что если функция возвратится до ожидаемого момента (например, при перехвате сигнала), ее можно будет вызвать еще раз, не изменяя содержимого структуры timespec. Например, если нам нужно ограничить время ожидания 3 минутами, то мы должны указать в этой структуре не 3 минуты, а текущее время + 3 минуты.
Для этого можно воспользоваться функцией gettimeofday (раздел 6.9), которая возвращает текущее время в структуре timeval, и затем заполнить соответствующие поля структуры timespec. Приводимая ниже функция выполняет указанные действия.
void maketimeout(struct timespec *tsp, long minutes)
{
struct timeval now;
gettimeofday(&now, NULL); /* получить текущее время */
tsp->tv_sec = now->tv_sec;
tsp->tv_nsec = now->tv_usec *1000; /* микросекунды в наносекунды */
tsp->tv_sec += minutes * 60; /* добавить время ожидания */
}
Если время ожидания истечет до возникновения события, функция pthread_cond_timedwait запрет взаимное исключение и вернет код ошибки ETIMEDOUT. Когда вызов какой-либо из функций pthread_cond_wait или pthread_cond_timedwait завершится успехом, поток должен заново проверить выполнение условия, поскольку может произойти ложное пробуждение при отсутствии выполнения условия. Различные реализации стремятся уменьшить количество ложных пробуждений, но они все равно происходят.
Для извещения приостановленных потоков о наступлении события предусмотрены также две функции. Функция pthread_cond_signal возобновит работу только одного потока, ожидающего наступления события, а функция pthread_cond_broadcast – всех потоков, ожидающих наступления события.
#include <pthread.h>
int pthread_cond_signal (pthread_cond_t *cond);
int pthread_cond_broadcast (pthread_cond_t *cond);
/* обе функции возвращают 0 в случае успеха, код ошибки – в случае неудачи */
Когда вызывается функция pthread_cond_signal, говорят, что посылается сигнал о наступлении события. Мы должны сделать все возможное, чтобы сигнал о наступлении события посылался только при выполнении условия.
Слово signal в имени первой из двух функций не имеет никакого отношения к сигналам Unix SIGxxx.
Мы проиллюстрируем использование условных переменных, переписав пример из раздела 15.2.2. Влистинге 15.3приведен код соответствующей программы. Код функцииmainпо сравнению слистингом 15.2не изменился. Функцииproduceиconsumeпретерпели некоторые изменения.
Листинг 15.3. Пример использования переменных состояния
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define min(a,b) ((a) < (b) ? (a) : (b))
#define MAXNITEMS 1000000
#define MAXNTHREADS 100
/* Глобальные переменные для всех потоков */
int res;
int nitems; /* Только для чтения производителем и потребителем */
int buff[MAXNITEMS];
struct
{
pthread_mutex_t mutex;
int nput; /* Индекс следующего сохраняемого элемента */
int nval; /* Следующее сохраняемое значение */
} put = { PTHREAD_MUTEX_INITIALIZER };
struct
{
pthread_mutex_t mutex;
pthread_cond_t cond;
int nready; /* Количество готовых элементов для потребителя */
} nready = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER };
void *produce(void *arg)
{
for ( ; ; )
{
if ((res = pthread_mutex_lock(&put.mutex)) != 0)
{
fprintf(stderr, "Ошибка запирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
if (put.nput >= nitems)
{ /* массив заполнен, работа окончена */
if ((res = pthread_mutex_unlock(&put.mutex)) != 0)
{
fprintf(stderr, "Ошибка отпирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
return(NULL);
}
buff[put.nput] = put.nval;
put.nput++;
put.nval++;
if ((res = pthread_mutex_unlock(&put.mutex)) != 0)
{
fprintf(stderr, "Ошибка отпирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
if ((res = pthread_mutex_lock(&nready.mutex)) != 0)
{
fprintf(stderr, "Ошибка запирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
if (nready.nready == 0)
if ((res = pthread_cond_signal(&nready.cond)) != 0)
{
fprintf(stderr, "Ошибка сигнализации: %s\n", strerror(res));
exit(1);
}
nready.nready++;
if ((res = pthread_mutex_unlock(&nready.mutex)) != 0)
{
fprintf(stderr, "Ошибка отпирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
*((int *) arg) += 1;
}
}
void *consume(void *arg)
{
int i;
for (i = 0; i < nitems; i++)
{
if ((res = pthread_mutex_lock(&nready.mutex)) != 0)
{
fprintf(stderr, "Ошибка запирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
while (nready.nready == 0)
if ((res = pthread_cond_wait(&nready.cond, &nready.mutex)) != 0)
{
fprintf(stderr, "Ошибка ожидания: %s\n", strerror(res));
exit(1);
}
nready.nready--;
if ((res = pthread_mutex_unlock(&nready.mutex)) != 0)
{
fprintf(stderr, "Ошибка отпирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
if (buff[i] != i)
printf("buff[%d] = %d\n", i, buff[i]);
}
return(NULL);
}
Две переменные, nput и nval, а также взаимное исключение, мы объединили в структуру с именем put. Эта структура используется производителями. Другая структура, nready, содержит счетчик, условную переменную и взаимное исключение. Мы инициализируем условную переменную значением PTHREAD_COND_INITIALIZER.
Перед помещением очередного элемента в массив buff для блокирования критической области в потоке-производителе теперь используется взаимное исключение put.mutex. Мы наращиваем значение счетчика nready.nready, в котором хранится количество элементов, готовых для обработки потребителем. Перед тем как нарастить значение, мы проверяем, не было ли значение счетчика нулевым, и если да, то вызывается функция pthread_cond_signal, позволяющая возобновить выполнение некоторого одного потока (в данном случае потребителя), ожидающего установки ненулевого значения этого счетчика. Теперь мы видим, как взаимодействуют взаимное исключение и связанная с ним условная переменная. Счетчик используется совместно производителями и потребителем, поэтому доступ к нему разрешается только при условии запирания соответствующего взаимного исключения (nready.mutex). Условная переменная используется для ожидания и передачи сигнала.
Потребитель просто ждет, пока значение счетчика nready.nready не станет отличным от нуля. Поскольку этот счетчик используется совместно с производителями, его значение можно проверять только при запертом соответствующем взаимном исключении. Если при проверке значение счетчика оказывается нулевым, мы вызываем функцию pthread_cond_wait для приостановки потока. При этом атомарно выполняются два действия:
Отпирается взаимное исключение nready.mutex.
Выполнение потока приостанавливается, пока какой-нибудь другой поток не вызовет pthread_cond_signal для соответствующей условной переменной (nready.cond).
Перед возвращением управления потоку функция pthread_cond_wait снова запирает взаимное исключение nready.mutex. Таким образом, если после возвращения из функции мы обнаруживаем, что счетчик имеет ненулевое значение, мы уменьшаем его значение на единицу (зная, что взаимное исключение заперто) и отпираем nready.mutex.
Код, проверяющий условие и приостанавливающий процесс, если оно не выполняется, обычно выглядит примерно так:
struct
{
pthread_mutex_t mutex;
pthread_cond_t cond;
переменные, для которых устанавливается условие
} var = { PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER, ... };
pthread_mutex_lock(&var.mutex);
while (условие ложно)
pthread_cond_wait(&var.cond, &var.mutex);
изменение условия
pthread_mutex_unlock(&var.mutex);
Код, передающий сигнал условной переменной, обычно выглядит примерно так:
pthread_mutex_lock(&var.mutex);
установка истинного значения условия
pthread_cond_signal(&var.cond);
pthread_mutex_unlock(&var.mutex);
В нашем примере переменная, для которой ставилось условие, представляла собой целочисленный счетчик, а установка условия означала просто увеличение значения счетчика. Мы улучшили код программы, посылая сигнал только при изменении значения счетчика с 0 на 1.
