Чтение онлайн

ЖАНРЫ

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

Самьюэл Алекс

Шрифт:

потока. */

 pthread_cleanup_push(deallocate_buffer, temp_buffer);

 /* Выполнение других действий... */

 /* Отмена регистрации обработчика. Поскольку функции передается

ненулевой аргумент, она выполняет очистку, вызывая функцию

deallocate_buffer. */

 pthread_cleanup_pop(1);

}

В данном случае функции

pthread_cleanup_pop
передается ненулевой аргумент, поэтому функция очистки
deallocate_buffer
вызывается автоматически. В данном простейшем случае можно было в качестве обработчика непосредственно использовать стандартную библиотечную функцию
free
.

4.3.2. Очистка потоковых данных в C++

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

Тем не менее, если поток вызывает функцию

pthread_exit
, среда выполнения C++ не может гарантировать вызов деструкторов для всех автоматических переменных, находящихся в стеке потока. Чтобы этого добиться, нужно вызвать функцию
pthread_exit
в рамках конструкции
try/catch
, охватывающей все тело потоковой функции. При этом перехватывается специальное исключение
ThreadExitException
.

Программа, приведенная в листинге 4.9, иллюстрирует данную методику. Потоковая функция сообщает о своем намерении завершить поток, генерируя исключение

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

Листинг 4.9. (cxx-exit.cpp) Безопасное завершение потока в C++

#include <pthread.h>

class ThreadExitException {

public:

 /* Конструктор, принимающий аргумент RETURN_VALUE, в котором

содержится возвращаемое потоком значение. */

 ThreadExitException(void* return_value) :

thread_return_value_(return_value) {

 }

 /* Реальное завершение потока. В программу возвращается

значение, переданное конструктору. */

 void* DoThreadExit {

pthread_exit(thread_return_value_);

 }

private:

 /* Значение, возвращаемое в программу при завершении потока. */

 void* thread_return_value_;

};

void do_some_work {

 while (1) {

/* Здесь выполняются основные действия... */

if (should_exit_thread_immediately)

throw ThreadExitException(/* поток возвращает */NULL);

 }

}

void* thread_function(void*) {

 try {

do_some_work;

 } catch (ThreadExitException ex) {

/*
Возникла необходимость завершить поток. */

ex.DoThreadExit;

 }

 return NULL;

}

4.4. Синхронизация потоков и критические секции

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

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

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

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;

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

Поделиться с друзьями: