- •Часть 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.2.1. Схема производитель-потребитель
Одна из классических задач синхронизации называется задачей производителя и потребителя. Она также известна как задача ограниченного буфера. Один или несколько производителей (потоков или процессов) создают данные, которые обрабатываются одним или несколькими потребителями. Эти данные передаются между производителями и потребителями с помощью одной из форм IPC.
С этой задачей мы регулярно сталкиваемся при использовании каналов Unix. Команда интерпретатора, использующая канал
grep pattern chapters.* | wc –l
является примером такой задачи. Программа grepвыступает как единственный производитель, аwc– как единственный потребитель. Канал используется как форма IPC. Требуемая синхронизация между производителем и потребителем обеспечивается ядром, обрабатывающим командыwriteпроизводителя иreadпотребителя. Если производитель опережает потребителя (канал переполняется), ядро приостанавливает производителя при вызовеwrite, пока в канале не появится место. Если потребитель опережает производителя (канал опустошается), ядро приостанавливает потребителя при вызовеread, пока в канале не появятся данные.
Такой тип синхронизации называется неявным; производитель и потребитель не знают о том, что синхронизация вообще осуществляется. Если бы мы использовали очередь сообщений System V в качестве средства IPC между производителем и потребителем, ядро снова взяло бы на себя обеспечение синхронизации.
При использовании разделяемой памяти как средства IPC между производителем и потребителем, однако, требуется использование какого-либо вида явной синхронизации. Мы продемонстрируем это, воспользовавшись взаимным исключением. Схема рассматриваемого примера изображена на рис. 15.4.

рис. 15.4
В одном процессе у нас имеется несколько потоков-производителей и один поток-потребитель. Целочисленный массив buffсодержит производимые и потребляемые данные (данные совместного пользования). Для простоты производители просто устанавливают значенияbuff[0]в 0,buff[1]в 1 и т. д. Потребитель перебирает элементы массива, проверяя правильность записей.
В этом первом примере мы концентрируем внимание на синхронизации между отдельными потоками-производителями. Поток-потребитель не будет запущен, пока все производители не завершат свою работу. В листинге 15.1приведена реализация нашего примера.
Листинг 15.1. Пример системы производитель-потребитель
#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; /* Только для чтения производителем и потребителем */
struct
{
pthread_mutex_t mutex;
int buff[MAXNITEMS];
int nput;
int nval;
} shared = { PTHREAD_MUTEX_INITIALIZER };
void *produce(void *arg)
{
for ( ; ; )
{
if ((res = pthread_mutex_lock(&shared.mutex)) != 0)
{
fprintf(stderr, "Ошибка запирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
if (shared.nput >= nitems)
{ /* массив заполнен, работа окончена */
if ((res = pthread_mutex_unlock(&shared.mutex)) != 0)
{
fprintf(stderr, "Ошибка отпирания взаимного исключения: %s\n",
strerror(res));
exit(1);
}
return(NULL);
}
shared.buff[shared.nput] = shared.nval;
shared.nput++;
shared.nval++;
if ((res = pthread_mutex_unlock(&shared.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 (shared.buff[i] != i)
printf("buff[%d] = %d\n", i, shared.buff[i]);
return(NULL);
}
int main(int argc, char *argv[])
{
int i, nthreads, count[MAXNTHREADS];
pthread_t tid_produce[MAXNTHREADS], tid_consume;
if (argc < 3)
{
printf("Использование: %s <число_элементов> <число_потоков>\n", argv[0]);
exit(-1);
}
nitems = min(atoi(argv[1]), MAXNITEMS);
nthreads = min(atoi(argv[2]), MAXNTHREADS);
if ((res = pthread_setconcurrency(nthreads)) != 0)
{
fprintf(stderr, "Ошибка при вызове функции pthread_setconcurrency: %s\n", strerror(res));
exit(1);
}
for (i = 0; i < nthreads; i++)
{ /* Запуск всех потоков-производителей */
count[i] = 0;
if ((res = pthread_create(&tid_produce[i], NULL, produce, &count[i])) != 0)
{
fprintf(stderr, "Ошибка создания потока-производителя номер %d: %s\n",
i, strerror(res));
exit(1);
}
}
for (i = 0; i < nthreads; i++)
{ /* Ожидание завершения всех потоков-производителей */
if ((res = pthread_join(tid_produce[i], NULL)) != 0)
{
fprintf(stderr, "Ошибка завершения потока-производителя номер %d: %s\n",
i, strerror(res));
exit(1);
}
printf("count[%d] = %d\n", i, count[i]);
} /* Запуск и ожидание завершения потока-потребителя */
if ((res = pthread_create(&tid_consume, NULL, consume, NULL)) != 0)
{
fprintf(stderr, "Ошибка создания потока-потребителя: %s\n", strerror(res));
exit(1);
}
if ((res = pthread_join(tid_consume, NULL)) != 0)
{
fprintf(stderr, "Ошибка завершения потока-потребителя: %s\n",
strerror(res));
exit(1);
}
exit(0);
}
Переменные, которые совместно используются потоками, объединены в структуру с именем sharedвместе с взаимным исключением, чтобы подчеркнуть, что доступ к переменным возможен только при условии запирания взаимного исключения. Переменнаяnputхранит индекс следующего элемента массиваbuff, подлежащего обработке, а переменнаяnvalсодержит следующее значение, которое должно быть помещено в массив (0, 1, 2 и т. д.). Мы выделяем память под эту структуру и статически инициализируем взаимное исключение, используемое для синхронизации действий потоков-производителей.
Формирование данных происходит внутри функции produce. Критическая область кода производителя состоит из проверки на достижение конца массива (завершение работы)
if (shared.nput >= nitems)
и трех строк, помещающих очередное значение в массив:
shared.buff[shared.nput] = shared.nval;
shared.nput++;
shared.nval++;
Мы защищаем эту область с помощью взаимного исключения, не забывая отпереть его после завершения работы. Обратите внимание, что увеличение элемента массива count(через указательarg) не относится к критической области, поскольку каждый поток имеет свой личный счетчик. Поэтому мы не включаем эту строку в критическую область, защищаемую взаимным исключением. Один из принципов хорошего стиля программирования заключается в минимизации объема кода, защищаемого взаимным исключением.
Потребитель проверяет правильность значений всех элементов массива внутри функции consumeи выводит сообщение в случае обнаружения ошибки. Эта функция запускается в единственном экземпляре и только после того, как все потоки-производители завершат свою работу, так что здесь синхронизация не требуется.
Первый аргумент командной строки указывает количество элементов, которые будут произведены производителями, а второй – количество запускаемых потоков-производителей, каждый из которых вызывает функцию produce. Идентификаторы потоков хранятся в массивеtid_produce. Аргументом каждого потока-производителя является адрес соответствующего элемента массиваcount. Счетчики инициализируют нулевым значением, и каждый поток увеличивает значение своего счетчика на 1 после помещения очередного элемента в буфер. Содержимое массива счетчиков затем выводится на экран, так что мы можем узнать, сколько элементов было помещено в буфер каждым из потоков.
Мы ожидаем завершения работы всех потоков-производителей, выводя содержимое счетчика для каждого потока, а затем запускаем единственный поток-потребитель. Таким образом (на данный момент) мы исключаем необходимость синхронизации между потребителем и производителями. Мы ждем завершения работы потока-потребителя, а затем завершаем работу всего процесса.
При запуске только что описанной программы с пятью потоками-производителями, которые должны вместе создать один миллион элементов данных, мы получим следующий результат:
$ ./a.out 1000000 5
count[0] = 41008
count[1] = 194203
count[2] = 181203
count[3] = 88766
count[4] = 494820
Если убрать из этого примера установку блокировки с помощью взаимного исключения, то он будет работать неправильно, как и предполагается. Потребитель обнаружит множество элементов buff[i], значения которых будут отличны отi. Также можно убедиться, что удаление блокировки ничего не изменит, если запустить на выполнение только один поток-производитель.
