KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » Марк Митчелл - Программирование для Linux. Профессиональный подход

Марк Митчелл - Программирование для Linux. Профессиональный подход

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Марк Митчелл, "Программирование для Linux. Профессиональный подход" бесплатно, без регистрации.
Перейти на страницу:

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

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

4.4.1. Состояние гонки

Предположим, что в программу поступает группа запросов, которые обрабатываются несколькими одновременными потоками. Очередь запросов представлена связанным списком объектов типа struct job.

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

Листинг 4.10. (job-queue1.c) Потоковая функция, работающая с очередью заданий

#include <malloc.h>


struct job {

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

 struct job* next;


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

};


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

struct job* job_queue;


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

void* thread_function(void* arg) {

 while (job_queue != NULL) {

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

  struct job* next_job = job_queue;

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

  job_queue = job_queue->next;

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

  process_job(next_job);

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

  free(next_job);

 }

 return NULL;

}

Теперь предположим, что два потока завершают свои операции примерно в одно и то же время, а в очереди остается только одно задание. Первый поток проверяет, равен ли указатель job_queue значению NULL, и, обнаружив, что очередь не пуста, входит в цикл, где сохраняет указатель на объект задания в переменной next_job. В этот момент Linux прерывает первый поток и активизирует второй. Он тоже проверяет указатель job_queue, устанавливает, что он не равен NULL, и записывает тот же самый указатель в свою переменную next_job. Увы, теперь мы имеем два потока, выполняющих одно и то же задание.

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

Это наглядный пример гонки за ресурсами. Если программе "повезет", система не распланирует потоки именно таким образом и ошибка не проявится. Возможно, только в сильно загруженной системе (или в новой многопроцессорной системе важного клиента!) произойдет "необъяснимый" сбой.

Чтобы исключить возможность гонки, необходимо сделать операции атомарными. Атомарная операция неделима и непрерывна; если она началась, то уже не может быть приостановлена или прервана, пока, наконец, не завершится. Выполнение других операций в это время становится невозможным. В нашем конкретном примере проверка переменной job_queue и удаление задания должны выполняться как одна атомарная операция.

4.4.2. Исключающие семафоры

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

Реализация такого решения требует поддержки от операционной системы. В Linux имеется специальное средство, называемое исключающим семафором, или мьютексом (MUTual EXclusion — взаимное исключение). Это специальная блокировка, которую в конкретный момент времени может устанавливать только одни поток. Если исключающий семафор захвачен каким-то потоком, другой поток, обращающийся к семафору, оказывается заблокированным или переведенным в режим ожидания. Как только семафор освобождается, поток продолжает свое выполнение. ОС Linux гарантирует, что между потоками, пытающимися захватить исключающий семафор, не возникнет гонка. Такой семафор может принадлежать только одному потоку, а все остальные потоки блокируются.

Чтобы создать исключающий семафор, нужно объявить переменную типа pthread_mutex_t и передать указатель на нее функции pthread_mutex_init(). Вторым аргументом этой функции является указатель на объект атрибутов семафора. Как и в случае функции pthread_create(), если объект атрибутов пуст, используются атрибуты по умолчанию. Переменная исключающего семафора инициализируется только один раз. Вот как это делается:

pthread_mutex_t mutex;

pthread_mutex_init(&mutex, NULL);

Более простой способ создания исключающего семафора со стандартными атрибутами — присвоение переменной специального значения PTHREAD_MUTEX_INITIALIZER. Вызывать функцию pthread_mutex_init() в таком случае не требуется. Это особенно удобно для глобальных переменных (а в C++ — статических переменных класса). Предыдущий фрагмент программы эквивалентен следующей записи:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

Поток может попытаться захватить исключающий семафор, вызвав функцию pthread_mutex_lock(). Если семафор свободен, он переходит во владение данного потока и функция немедленно завершается. Если же семафор уже был захвачен другим потоком. выполнение функции pthread_mutex_lock() блокируется и возобновляется только тогда, когда семафор вновь становится свободным. Сразу несколько потоков могут ожидать освобождения исключающего семафора. Когда это событие наступает, только один поток (выбираемый произвольным образом) разблокируется и получает возможность захватить семафор; остальные потоки остаются заблокированными.

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

В листинге 4.11 представлена другая версия программы, работающей с очередью заданий. Теперь очередь "защищена" исключающим семафором. Прежде чем получить доступ к очереди (для чтения или записи), каждый поток сначала захватывает семафор. Только когда вся последовательность операций проверки очереди и удаления задания из нее будет закончена, произойдет освобождение семафора. Благодаря этому не возникает описанное выше состояние гонки.

Листинг 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;

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

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*