Скачиваний:
90
Добавлен:
12.05.2015
Размер:
913.92 Кб
Скачать

15.3.2. Исключение состояния гонок

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

int dosignal;

pthread_mutex_lock(&nready.mutex);

dosignal = (nready.nready == 0);

nready.nready++;

pthread_mutex_unlock(&nready.mutex);

if (dosignal)

pthread_cond_signal(&nready.cond);

Здесь мы отправляем сигнал условной переменной только после отпирания взаимного исключения. Это разрешено стандартом Posix: поток, вызывающий функцию pthread_cond_signal, не обязан перед этим запирать взаимное исключение, связанное с условной переменной. Такой вариант, когда сигнал посылается после отпирания взаимного исключения, будет прекрасно работать, даже если какой-либо поток успеет возобновить работу до передачи сигнала. Поскольку наступление события проверяется в цикле while, это не представляет проблемы: поток просто возобновит работу, убедится, что буфер пуст, и опять перейдет в режим ожидания. Однако стандарт Posix говорит, что если требуется предсказуемое поведение при одновременном выполнении потоков, исключающее подобные гонки, то тогда необходимо сначала вызвать функцию pthread_cond_signal, а только после этого отпереть взаимное исключение.

15.4. Блокировки чтения-записи

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

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

В этом разделе описываются блокировки чтения-записи, причем существует различие между получением такой блокировки для считывания и для записи. Правила действуют следующие:

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

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

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

Если блокировка чтения-записи установлена в режиме для записи, все потоки, которые будут пытаться установить эту блокировку, будут приостановлены до тех пор, пока блокировка не будет снята. Если блокировка чтения-записи установлена в режиме для чтения, все потоки, которые будут пытаться установить эту блокировку для чтения, получат доступ к ресурсу, но если какой-либо поток попытается установить режим для записи, он будет приостановлен до тех пор, пока не будет снята последняя блокировка для чтения. Реализации блокировок чтения-записи в различных ОС могут значительно различаться, но обычно, если блокировка для чтения уже установлена и имеется поток, который пытается установить блокировку для записи, то остальные потоки, которые пытаются установить блокировку для чтения, будут приостановлены. Это предотвращает возможность блокирования пишущих потоков непрекращающимися запросами на установку блокировки для чтения.

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

Такой вид совместного доступа к ресурсу также носит название совместно-исключающей блокировки (shared-exclusive), поскольку тип используемой блокировки на чтение называется совместной блокировкой (shared lock), а тип используемой блокировки на запись называется исключающей блокировкой (exclusive lock). Существует также специальное название для данной задачи (несколько считывающих процессов и один записывающий): задача читателей и писателей (readers and writers problem), и говорят также о блокировке читателей и писателя (readers-writer lock). В последнем случае слово “читатель” специально употреблено во множественном числе, а “писатель” – в единственном, чтобы подчеркнуть сущность задачи.

Блокировка чтения-записи имеет тип pthread_rwlock_t. Если переменная этого типа размещается статически, ей необходимо присвоить значение константы PTHREAD_RWLOCK_INITIALIZER. Подобно взаимным исключениям, динамически размещаемые блокировки чтения-записи должны быть инициализированы перед их использованием и разрушены перед освобождением занимаемой ими памяти.

#include <pthread.h>

int pthread_rwlock_init (pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);

/* обе функции возвращают 0 в случае успеха, код ошибки – в случае неудачи */

Функция pthread_rwlock_init инициализирует блокировку чтения-записи. Если в аргументе attr передается пустой указатель, блокировка инициализируется с атрибутами по умолчанию. Атрибуты блокировок чтения-записи мы рассмотрим в разделе 12.4.

Перед освобождением памяти, занимаемой блокировкой чтения-записи, нужно вызвать функцию pthread_rwlock_destroy, чтобы освободить все занимаемые блокировкой ресурсы. Функция pthread_rwlock_init размещает все необходимые для блокировки ресурсы, а функция pthread_rwlock_destroy освобождает их. Если освободить память, занимаемую блокировкой чтения-записи, без предварительного обращения к функции pthread_rwlock_destroy, то все ресурсы, используемые блокировкой, будут потеряны для процесса.

Функция pthread_rwlock_rdlockпозволяет заблокировать ресурс для чтения, причем вызвавший поток будет приостановлен, если блокировка чтения-записи уже установлена записывающим потоком. Функцияpthread_rwlock_wrlockпозволяет заблокировать ресурс для записи, причем вызвавший поток будет приостановлен, если блокировка чтения-записи уже установлена каким-либо другим потоком (считывающим или записывающим). Функцияpthread_rwlock_unlockснимает блокировку любого типа (чтения или записи).

#include <pthread.h>

int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);

/* все три функции возвращают 0 в случае успеха, код ошибки – в случае неудачи */

Реализации могут ограничивать количество блокировок, установленных в режиме совместного использования, поэтому обязательно нужно проверять значение, возвращаемое функцией pthread_rwlock_rdlock. Даже когда функции pthread_rwlock_wrlock и pthread_rwlock_unlock возвращают код ошибки, нет необходимости проверять значения, которые возвращают эти функции, если схема установки блокировок разработана надлежащим образом. Эти функции могут вернуть код ошибки только в том случае, когда блокировка не была инициализирована или когда может возникнуть тупиковая ситуация при попытке повторно установить уже установленную блокировку.

Стандарт SUS определяет дополнительные версии примитивов для работы с блокировками, которые могут использоваться для установки или проверки состояния блокировки.

#include <pthread.h>

int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);

int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);

/* обе функции возвращают 0 в случае успеха, код ошибки – в случае неудачи */

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

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

Листинг 15.4. Использование блокировки чтения-записи

#define _GNU_SOURCE

#include <pthread.h>

#include <stdio.h>

#include <stdlib.h>

#include <time.h>

#define min(a,b) ((a) < (b) ? (a) : (b))

#define MAXNITEMS 1000000

#define MAXNTHREADS 100

int nitems, nthreads; /* Только для чтения производителем и потребителем */

pthread_t tid_consume[MAXNTHREADS];

struct job

{ /* задание */

struct job *j_next;

struct job *j_prev;

pthread_t j_id; /* Идентификатор потока для обработки этого задания */

/* ... другие поля структуры ... */

};

struct queue

{ /* очередь заданий */

struct job *q_head;

struct job *q_tail;

int num_proc_items; /* Количество обработанных заданий */

pthread_rwlock_t q_lock;

} work_q = { NULL, NULL, 0, PTHREAD_RWLOCK_INITIALIZER };

void insert_job(struct queue *qp, struct job *jp)

{ /* Вставить задание в начало очереди */

int res;

if ((res = pthread_rwlock_wrlock(&qp->q_lock)) != 0)

{

fprintf(stderr, "Ошибка установки блокировки записи: %s\n", strerror(res));

exit(1);

}

jp->j_next = qp->q_head;

jp->j_prev = NULL;

if (qp->q_head != NULL)

qp->q_head->j_prev = jp;

else

qp->q_tail = jp; /* список был пуст */

qp->q_head = jp;

if ((res = pthread_rwlock_unlock(&qp->q_lock)) != 0)

{

fprintf(stderr, "Ошибка снятия блокировки записи: %s\n", strerror(res));

exit(1);

}

}

void append_job(struct queue *qp, struct job *jp)

{ /* Добавить задание в конец очереди */

int res;

if ((res = pthread_rwlock_wrlock(&qp->q_lock)) != 0)

{

fprintf(stderr, "Ошибка установки блокировки записи: %s\n", strerror(res));

exit(1);

}

jp->j_next = NULL;

jp->j_prev = qp->q_tail;

if (qp->q_tail != NULL)

qp->q_tail->j_next = jp;

else

qp->q_head = jp; /* список был пуст */

qp->q_tail = jp;

if ((res = pthread_rwlock_unlock(&qp->q_lock)) != 0)

{

fprintf(stderr, "Ошибка снятия блокировки записи: %s\n", strerror(res));

exit(1);

}

}

void remove_job(struct queue *qp, struct job *jp)

{ /* Удалить задание из очереди */

/* Блокировка записи устанавливается в функции consume */

if (jp == qp->q_head)

qp->q_head = jp->j_next;

else

jp->j_prev->j_next = jp->j_next;

if (jp == qp->q_tail)

qp->q_tail = jp->j_prev;

else

jp->j_next->j_prev = jp->j_prev;

/* Блокировка записи снимается в функции consume */

}

struct job *find_job(struct queue *qp, pthread_t id)

{ /* Найти задание для потока с заданным идентификатором */

struct job *jp;

/* Блокировка чтения устанавливается в функции consume */

for (jp = qp->q_head; jp != NULL; jp = jp->j_next)

if (pthread_equal(jp->j_id, id))

break;

return(jp);

/* Блокировка чтения снимается в функции consume */

}

void *produce(void *arg)

{

int i, num;

struct job *cur_job;

srand((unsigned int) time(NULL));

for (i = 0; i<nitems; i++)

{ /* Выделяем память для задания */

if ((cur_job = malloc(sizeof(struct job))) == NULL)

{

fprintf(stderr, "Ошибка вызова функции malloc для %d-го элемента\n", i);

exit(1);

} /* Назначаем поток для обработки задания случайным образом */

num = rand() % nthreads;

cur_job->j_id = tid_consume[num];

if ((num % 2) == 1)

insert_job(&work_q,cur_job);

else

append_job(&work_q,cur_job);

} /* Задание поставлено в очередь */

return(NULL);

}

void *consume(void *arg)

{

int res;

struct job *proc_job;

for ( ; ; )

{

if ((res = pthread_rwlock_rdlock(&work_q.q_lock)) != 0)

{

fprintf(stderr, "Ошибка установки блокировки чтения: %s\n",

strerror(res));

exit(1);

}

if (work_q.num_proc_items >= nitems)

{ /* все задания обработаны, работа окончена */

if ((res = pthread_rwlock_unlock(&work_q.q_lock)) != 0)

{

fprintf(stderr, "Ошибка снятия блокировки чтения: %s\n", strerror(res));

exit(1);

}

return(NULL);

} /* Поиск задания в очереди */

proc_job = find_job(&work_q,pthread_self());

if ((res = pthread_rwlock_unlock(&work_q.q_lock)) != 0)

{

fprintf(stderr, "Ошибка снятия блокировки чтения: %s\n", strerror(res));

exit(1);

}

if (proc_job)

{ /* Поток нашел задание для обработки */

if ((res = pthread_rwlock_wrlock(&work_q.q_lock)) != 0)

{

fprintf(stderr, "Ошибка установки блокировки записи: %s\n",

strerror(res));

exit(1);

} /* Удаление задания из очереди */

remove_job(&work_q, proc_job);

work_q.num_proc_items++;

if ((res = pthread_rwlock_unlock(&work_q.q_lock)) != 0)

{

fprintf(stderr, "Ошибка снятия блокировки записи: %s\n", strerror(res));

exit(1);

}

*((int *) arg) += 1;

free(proc_job); /* Освобождаем память, занятую заданием */

}

}

}

int main(int argc, char *argv[])

{

int i, res, count[MAXNTHREADS];

pthread_t tid_produce;

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 + 1)) != 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_consume[i], NULL, consume, &count[i])) != 0)

{

fprintf(stderr, "Ошибка создания потока-потребителя номер %d: %s\n", i, strerror(res));

exit(1);

}

}

if ((res = pthread_create(&tid_produce, NULL, produce, NULL)) != 0)

{

fprintf(stderr, "Ошибка создания потока-производителя: %s\n", strerror(res));

exit(1);

}

/* Ожидание завершения потока-производителя и всех потребителей */

if ((res = pthread_join(tid_produce, NULL)) != 0)

{

fprintf(stderr, "Ошибка завершения потока-производителя: %s\n", strerror(res));

exit(1);

}

for (i = 0; i < nthreads; i++)

{

if ((res = pthread_join(tid_consume[i], NULL)) != 0)

{

fprintf(stderr, "Ошибка завершения потока-потребителя номер %d: %s\n", i, strerror(res));

exit(1);

}

printf("count[%d] = %d\n", i, count[i]);

}

exit(0);

}

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

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

Соседние файлы в папке Chapter.4