Выбрать главу
Листинг 4.11. (job-queue2.c) Работа с очередью заданий, защищенной исключающим семафором

#include <malloc.h>

#include <pthread.h>

struct job {

 /* Ссылка на следующий элемент связанного списка. */

 struct job* next;

 /* Другие поля, описывающие требуемую операцию... */

};

/* Список отложенных заданий. */

struct job* job_queue;

/* Исключающий семафор, защищающий очередь. */

pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;

/* Обработка заданий до тех пор, пока очередь не опустеет. */

void* thread_function(void* arg) {

 while (1) {

  struct job* next_job;

  /* Захват семафора, защищающего очередь. */

  pthread_mutex_lock(&job_queue_mutex);

  /* Теперь можно проверить, является ли очередь пустой. */

  if (job_queue == NULL)

   next_job = NULL;

  else {

   /* Запрашиваем следующее задание. */

   next_job = job_queue;

   /* Удаляем задание из списка. */

   job_queue = job_queue->next;

  }

  /* Освобождаем семафор, так как работа с очередью окончена. */

  pthread_mutex_unlock(&job_queue_mutex);

  /* Если очередь пуста, завершаем поток. */

  if (next_job == NULL)

   break;

  /* Выполняем задание. */

  process_job(next_job);

  /* Очистка. */

  free(next_job);

 }

 return NULL;

}

Все операции доступа к совместно используемому указателю job_queue происходят между вызовами функций pthread_mutex_lock() и pthread_mutex_unlock(). Объект задания, ссылка на который хранится в переменной next_job, обрабатывается только после того, как ссылка на него удаляется из очереди, что позволяет обезопасить этот объект от других потоков.

Обратите внимание на то, что, если очередь пуста (т.е. указатель job_queue равен NULL), цикл не завершается немедленно. Это привело бы к тому, что исключающий семафор так и остался бы в захваченном состоянии и не позволил бы ни одному другому потоку получить доступ к очереди заданий. Мы действуем иначе: записываем в переменную next_job значение NULL и выходим из цикла только после освобождения семафора.

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

void enqueue_job(struct job* new_job) {

 pthread_mutex_lock(&job_queue_mutex);

 new_job->next = job_queue;

 job_queue = new_job;

 pthread_mutex_unlock(&job_queue_mutex);

}

4.4.3. Взаимоблокировки исключающих семафоров

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

Простейшая тупиковая ситуация — когда один поток пытается захватить тот же самый исключающий семафор дважды подряд. Дальнейшие действия зависят от типа исключающего семафора. Их всего три.

■ Захват быстрого семафора (используется по умолчанию) приведет к взаимоблокировке. Функция, обращающаяся к захваченному семафору данного типа, заблокирует поток до тех пор, пока семафор не будет освобожден. Но семафор принадлежит самому потоку, поэтому блокировка никогда не будет снята.

■ Захват рекурсивного семафора не приведет к взаимоблокировке. Семафор данного типа запоминает, сколько раз функция pthread_mutex_lock() была вызвана в потоке, которому принадлежит семафор. Чтобы освободить семафор и позволить другим потокам обратиться к нему, необходимо аналогичное число раз вызвать функцию pthread_mutex_unlock().

■ Операционная система Linux обнаруживает попытку повторно захватить контролирующий семафор и сигнализирует об этом: при очередном вызове функции pthread_mutex_lock() возвращается код ошибки EDEADLK.

По умолчанию в Linux создается быстрый семафор. В двух других случаях требуется предварительно создать объект атрибутов семафора, объявив переменную типа pthread_mutexattr_t и передав указатель на нее функции pthread_mutexattr_init(). Затем нужно задать тип исключающего семафора с помощью функции pthread_mutexattr_setkind_np(). Первым ее аргументом является указатель на объект атрибутов семафора; второй аргумент равен PTHREAD_MUTEX_RECURSIVE_NP в случае рекурсивного семафора и PTHREAD_MUTEX_ERRORCHECK_NP — в случае контролирующего семафора. Указатель на полученный объект атрибутов необходимо передать функции pthread_mutex_init(), которая создаст семафор. После этого нужно удалить объект атрибутов с помощью функции pthread_mutexattr_destroy().