Опубликован: 06.12.2004 | Доступ: свободный | Студентов: 1096 / 126 | Оценка: 4.76 / 4.29 | Длительность: 20:58:00
ISBN: 978-5-9556-0021-5
Лекция 1:

Потоки управления

Лекция 1: 1234567 || Лекция 2 >

Создание и терминирование потоков управления

Для создания нового потока управления служит функция pthread_create() (см. листинг 1.24).

#include <pthread.h>
int pthread_create (
    pthread_t *restrict thread,
    const pthread_attr_t *restrict attr,
    void *(*start_routine) (void *), 
    void *restrict arg);
Листинг 1.24. Описание функции pthread_create().

Выполнение созданного потока управления начнется с вызова (*start_routine) (arg); возврат из этой функции приведет к терминированию потока с возвращаемым значением в качестве статуса завершения. Из этого правила стандартом предусмотрено одно, вполне естественное исключение: для потока, выполнение которого началось с функции main(), возврат из нее означает завершение процесса, содержащего поток, со всеми вытекающими отсюда последствиями.

Аргумент attr задает атрибуты нового потока ; если значение attr равно NULL, используются зависящие от реализации подразумеваемые атрибуты.

От "родительского" вновь созданный поток управления наследует немногое: маску сигналов и характеристики вещественной арифметики.

К числу средств создания потоков можно отнести и функцию fork(). Правда, здесь нас будет интересовать не она сама, а ассоциированные с ней обработчики, зарегистрированные с помощью функции pthread_atfork() (см. листинг 1.25).

#include <pthread.h>
int pthread_atfork (
    void (*prepare) (void), 
    void (*parent) (void),
    void (*child) (void));
Листинг 1.25. Описание функции pthread_atfork().

В каждом обращении к pthread_atfork() фигурируют три обработчика (если, конечно, в качестве значения аргумента не задан пустой указатель). Первый ( (*prepare)() ) выполняется в контексте потока, вызвавшего fork(), до разветвления процесса ; второй ( (*parent)() ) – в том же контексте, но после разветвления; третий ( (*child)() ) – в контексте единственного потока порожденного процесса.

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

Как и процесс, поток управления можно терминировать изнутри и извне. С одним, неявным, но наиболее естественным способом "самоликвидации" – выходом из стартовой функции потока – мы уже познакомились. Тот же эффект достигается вызовом функции pthread_exit() (см. листинг 1.26).

#include <pthread.h>
void pthread_exit (void *value_ptr);
Листинг 1.26. Описание функции pthread_exit().

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

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

#include <pthread.h>
int pthread_join (
    pthread_t thread, void **value_ptr_ptr);
Листинг 1.27. Описание функции pthread_join().

Поток управления, вызвавший функцию pthread_join(), приостанавливает выполнение до завершения потока, идентификатор которого задан аргументом thread. При успешном возврате из pthread_join() результат, как и положено, равен нулю, а по указателю value_ptr_ptr (если он не пуст) помещается значение (указатель value_ptr ), переданное в качестве аргумента функции pthread_exit(). Тем самым ждуший поток получает данные о статусе завершения ожидаемого.

Отметим, что трактовка значения value_ptr возлагается на приложение. Например, оно может считать его целым числом, а не указателем; по этой причине операционная система при выполнении функции pthread_exit() не вправе выдавать ошибку типа "неверный адрес" каким бы ни был аргумент value_ptr. Если он все же является указателем, то ему нельзя присваивать адрес автоматической переменной, поскольку к моменту его использования ожидаемый поток уже завершится и состояние его автоматических переменных станет неопределенным.

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

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

Третье общее соображение состоит в том, что такое критически важное событие, как завершение потока управления, не может оставаться без функций-обработчиков. Стандартом POSIX-2001 предусмотрено существование не одного, а целого стека подобных обработчиков, ассоциированного с потоком управления. Операции над этим стеком возложены на функции pthread_cleanup_push() и pthread_cleanup_pop() (см. листинг 1.28).

#include <pthread.h>

void pthread_cleanup_push (
     void (*routine) (void *), void *arg);

void pthread_cleanup_pop (int execute);
Листинг 1.28. Описание функций pthread_cleanup_push() и pthread_cleanup_pop().

Функция pthread_cleanup_push() помещает заданный аргументами routine и arg обработчик в стек обработчиков вызывающего потока. Функция pthread_cleanup_pop() извлекает верхний обработчик из этого стека и, если значение аргумента execute отлично от нуля, вызывает его (как (*routine) (arg) ).

Разумеется, все обработчики, начиная с верхнего, извлекаются из стека и вызываются при терминировании потока управления (вне зависимости от того, объясняется ли терминирование внутренними или внешними причинами). В частности, это происходит после того, как поток обратится к функции pthread_exit().

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

Пару функций pthread_cleanup_push() и pthread_cleanup_pop() можно представлять себе как открывающую и закрывающую скобки, оформленные в виде отдельных инструкций языка C и обрамляющие обслуживаемый обработчиком участок программы. Согласно стандарту POSIX-2001, этот участок должен представлять собой фрагмент одной лексической области видимости (блока), а pthread_cleanup_push() и pthread_cleanup_pop() могут быть реализованы как макросы (см. листинг 1.29).

#define pthread_cleanup_push (rtn, arg) { \
    struct _pthread_handler_rec \
    __cleanup_handler, \
    **__head; \
    __cleanup_handler.rtn = rtn; \
    __cleanup_handler.arg = arg; \
    (void) pthread_getspecific \
    (_pthread_handler_key, &__head); \
    __cleanup_handler.next = *__head; \
    *__head = &__cleanup_handler;

#define pthread_cleanup_pop (ex) \
    *__head = __cleanup_handler.next; \
    if (ex) (*__cleanup_handler.rtn) \
                (__cleanup_handler.arg); \
}
Листинг 1.29. Возможная реализация функций pthread_cleanup_push() и pthread_cleanup_pop() как макросов.

Обратим внимание на то, что в определении макроса pthread_cleanup_push() открывается внутренний блок, в котором декларируются два необходимых объекта – структура __cleanup_handler, описывающая обработчик, и указатель __head на вершину стека, представленного в виде односвязанного (линейного) списка. В определении pthread_cleanup_pop() этот блок закрывается. Так что даже из соображений синтаксической корректности вызовы pthread_cleanup_push() и pthread_cleanup_pop() должны быть парными и располагаться в одном блоке, но более существенной нам представляется корректность семантическая.

Лекция 1: 1234567 || Лекция 2 >
Павел Храмцов
Павел Храмцов
Россия
Денис Комаров
Денис Комаров
Россия, Москва