Уильям Стивенс - UNIX: взаимодействие процессов
Упражнения
1. Удалите взаимное исключение из листинга 7.2 и убедитесь, что программа работает неправильно, если одновременно запущено более одного производителя.
2. Что произойдет с листингом 7.1, если убрать вызов Pthread_join для потока-потребителя?
3. Напишите пpoгрaммy, вызывающую pthread_mutexatt_init и pthread_condattr_init в бесконечном цикле. Следите за используемой этим процессом памятью с помощью какой-нибудь программы, например ps. Что происходит? Теперь добавьте вызовы pthread_mutexattr_destroy и pthread_condattr_destroy и убедитесь, что утечки памяти нет.
4. В программе из листинга 7.6 производитель вызывает pthread_cond_signal только при изменении nready.nready с 0 на 1. Чтобы убедиться в эффективности этой оптимизации, вызывайте pthread_cond_signal каждый раз, когда nready.nready увеличивается на 1, и выведите его значение в главном потоке после завершения работы потребителя.
ГЛАВА 8
Блокировки чтения-записи
8.1. Введение
Взаимное исключение используется для предотвращения одновременного доступа нескольких потоков к критической области. Критическая область кода обычно содержит операции считывания или изменения данных, используемых потоками совместно. В некоторых случаях можно провести различие между считыванием данных и их изменением.
В этой главе описываются блокировки чтения-записи, причем существует различие между получением такой блокировки для считывания и для записи. Правила действуют следующие:
1. Любое количество потоков могут заблокировать ресурс для считывания, если ни один процесс не заблокировал его на запись.
2. Блокировка чтения-записи может быть установлена на запись, только если ни один поток не заблокировал ресурс для чтения или для записи.
Другими словами, произвольное количество потоков могут считывать данные, если ни один поток не изменяет их в данный момент. Данные могут быть изменены, только если никто другой их не считывает и не изменяет.
В некоторых приложениях данные считываются чаще, чем изменяются, поэтому такие приложения выиграют в быстродействии, если при их реализации будут использованы блокировки чтения-записи вместо взаимных исключений. Возможность совместного считывания данных произвольным количеством процессов позволит выполнять операции параллельно, и при этом данные все равно будут защищены от других потоков на время изменения их данным потоком.
Такой вид совместного доступа к ресурсу также носит название совместно-исключающей блокировки (shared-exclusive), поскольку тип используемой блокировки на чтение называется совместной блокировкой (shared lock), а тип используемой блокировки на запись называется исключающей блокировкой (exclusive lock). Существует также специальное название для данной задачи (несколько считывающих процессов и один записывающий): задача читателей и писателей (readers and writers problem), и говорят также о блокировке читателей и писателя (readers-writer lock). В последнем случае слово «читатель» специально употреблено во множественном числе, а «писатель» — в единственном, чтобы подчеркнуть сущность задачи.
ПРИМЕЧАНИЕ
Обычным примером, иллюстрирующим необходимость использования блокировок чтения-записи, является банковский счет. Считывать баланс со счета могут несколько потоков одновременно, но если какой-либо поток захочет изменить данные, ему придется подождать, пока считывающие потоки не закончат свои операции, и только после этого ему будет разрешено вносить изменения. До окончания процесса записи никакие другие процессы не должны получить доступа к счету.
Функции, описываемые в этой главе, определены стандартом Unix 98, поскольку блокировки чтения-записи не были частью стандарта Posix.1 1996 года. Эти функции были разработаны группой производителей Unix, известной под названием Aspen Group, в 1995 году вместе с другими расширениями, которые еще не были определены Posix.1. Рабочая группа Posix (1003.1j) в настоящее время разрабатывает набор расширений Pthreads, включающий блокировки чтения-записи, который, хочется верить, совпадет с описываемым в этой главе.
8.2. Получение и сброс блокировки чтения-записи
Блокировка чтения-записи имеет тип pthread_rwlock_t. Если переменная этого типа является статической, она может быть проинициализирована присваиванием значения константы PTHREAD_RWLOCK_INITIALIZER.
Функция pthread_rwlock_rdlock позволяет заблокировать ресурс для чтения, причем вызвавший процесс будет заблокирован, если блокировка чтения-записи уже установлена записывающим процессом. Функция pthread_rwlock_wrlock позволяет заблокировать ресурс для записи, причем вызвавший процесс будет заблокирован, если блокировка чтения-записи уже установлена каким-либо другим процессом (считывающим или записывающим). Функция pthread_rwlock_unlock снимает блокировку любого типа (чтения или записи):
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr );
/* Все функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */
Следующие две функции производят попытку заблокировать ресурс для чтения или записи, но если это невозможно, возвращают ошибку с кодом EBUSY, вместо того чтобы приостановить выполнение вызвавшего процесса:
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */
8.3. Атрибуты блокировки чтения-записи
Мы уже отмечали, что статическая блокировка может быть проинициализирована присваиванием ей значения PTHREAD_RWLOCK_INITIALIZER. Эти переменные могут быть проинициализированы и динамически путем вызова функции pthread_rwlock_init.
Когда поток перестает нуждаться в блокировке, он может вызвать pthread_rwlock_ destroy:
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);
/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */
Если при инициализации блокировки чтения-записи attr представляет собой нулевой указатель, атрибутам присваиваются значения по умолчанию. Для присваивания атрибутам других значений следует воспользоваться двумя нижеследующими функциями:
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */
После инициализации объекта типа pthread_rwlockattr_t для установки или сброса отдельных атрибутов используются специальные функции. Единственный определенный на настоящее время атрибут — PTHREAD_PROCESS_SHARED, который указывает на то, что блокировка используется несколькими процессами, а не отдельными потоками одного процесса. Две приведенные ниже функции используются для получения и установки значения этого атрибута:
#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *valptr);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int value );
/* Обе функции возвращают 0 в случае успешного завершения, положительное значение Еххх – в случае ошибки */
Первая функция возвращает текущее значение в целом, на которое указывает аргумент valptr. Вторая функция устанавливает значение этого атрибута равным value, которое может быть либо PTHREAD_PROCESS_PRIVATE, либо PTHREAD_ PROCESS_SHARED.
8.4. Реализация с использованием взаимных исключений и условных переменных
Для реализации блокировок чтения-записи достаточно использовать взаимные исключения и условные переменные. В этом разделе мы рассмотрим одну из возможных реализаций, в которой предпочтение отдается ожидающим записи потокам. Это не является обязательным; возможны альтернативы.
ПРИМЕЧАНИЕ
Этот и последующие разделы данной главы содержат усложненный материал, который можно при первом чтении пропустить.
Другие реализации блокировок чтения записи заслуживают отдельного изучения. В разделе 7.1.2 книги [3] представлена реализация, в которой приоритет имеют ожидающие записи потоки и предусмотрена обработка отмены выполнения потока (о которой мы вскоре будем говорить подробнее). В разделе В.18.2.3.1 стандарта IEEE 1996 [8] представлена другая реализация, в которой предпочтение имеют ожидающие записи потоки и в которой также предусмотрена обработка отмены. В главе 14 книги [12] также приводится возможная реализация, в которой приоритет имеют ожидающие записи процессы. Реализация, приведенная в этом разделе, взята из пакета АСЕ (http://www.cs.wustl.edu/~schmidt/ACE.html), автором которого является Дуг Шмидт (Doug Schmidt). Аббревиатура АСЕ означает Adaptive Communications Environment. Во всех четырех реализациях используются взаимные исключения и условные переменные.