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

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

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

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

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

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

Заказать терминирование извне потока управления с заданным идентификатором можно, воспользовавшись функцией pthread_cancel() (см. листинг 1.30).

#include <pthread.h>
int pthread_cancel (pthread_t thread);
Листинг 1.30. Описание функции pthread_cancel().

Напомним, что на выполнение "заказа" влияют состояние восприимчивости и тип терминирования, установленные для потока, а также достижение точки терминирования. Эти атрибуты опрашиваются и изменяются с помощью функций pthread_setcancelstate(), pthread_setcanceltype() и pthread_testcancel() (см. листинг 1.31).

#include <pthread.h>

int pthread_setcancelstate (
    int state, int *oldstate);

int pthread_setcanceltype (
    int type, int *oldtype);

void pthread_testcancel (void);
Листинг 1.31. Описание функций pthread_setcancelstate(), pthread_setcanceltype(), pthread_testcancel().

Функции pthread_setcancelstate() и pthread_setcanceltype() атомарным образом, в рамках неделимой транзакции устанавливают новые значения ( state и type ) для состояния восприимчивости и типа и помещают по заданным указателям (соответственно, oldstate и oldtype ) старые значения. Допустимыми значениями для состояния восприимчивости к терминированию являются PTHREAD_CANCEL_ENABLE ( терминирование разрешено – подразумеваемое значение для вновь созданных потоков управления ) и PTHREAD_CANCEL_DISABLE, для типа – PTHREAD_CANCEL_DEFERRED ( отложенное терминирование – подразумеваемое значение) и PTHREAD_CANCEL_ASYNCHRONOUS (немедленное, асинхронное терминирование ).

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

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

Возвращаясь к функции pthread_cancel(), отметим, что иногда обработку заказа на терминирование сравнивают с реакцией на доставку сигнала. На наш взгляд, к подобной аналогии нужно относиться осторожно, поскольку она довольно поверхностна и основана в первую очередь на том, что и сигналы, и заказы на терминирование являются средствами асинхронного программного воздействия на потоки управления, и других механизмов подобной направленности стандарт POSIX-2001 не предусматривает. В частности, доставка как сигналов, так и заказов на терминирование способна вывести поток управления из состояния ожидания, быть может, потенциально бесконечного. Еще одна параллель – запрещение терминирования при входе в обработчик завершения и блокирование сигнала при входе в функцию его обработки.

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

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

Помимо заказа на терминирование, потоку управления можно направить и "честный" сигнал, воспользовавшись средством "внутрипроцессного межпотокового" взаимодействия – функцией pthread_kill() (см. листинг 1.32).

#include <signal.h>
int pthread_kill (
    pthread_t thread, int sig);
Листинг 1.32. Описание функции pthread_kill().

Как и в случае функции kill(), при нулевом значении аргумента sig проверяется корректность заданного идентификатора потока, но никакой сигнал не генерируется.

Напомним (см. курс [1]), что сигналы генерируются для конкретного потока управления (так, в частности, поступает функция pthread_kill() ) или для процесса в целом (как это делает функция kill() ), но доставляются они всегда одному потоку, только во втором случае его выбор определяется реализацией из соображений простоты доставки: обычно берется активный поток, если он не блокирует данный сигнал. Естественно, если для сигнала определена функция обработки, она выполняется в контексте целевого потока управления. Другие возможные действия ( терминирование, остановка) всегда применяются к процессу в целом.

Для иллюстрации изложенного приведем небольшую программу (см. листинг 1.33), в которой создается поток управления с последующей доставкой ему сигнала SIGINT. Возможные результаты работы этой программы показаны на листинге 1.34.

/* * * * * * * * * * * * * * * * * */
/* Программа демонстрирует генерацию     */
/* и доставку сигналов             */
/* потокам управления             */
/* * * * * * * * * * * * * * * * * */

#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>

/* * * * * * * * * * * * * * /
/* Функция обработки сигнала */
/* * * * * * * * * * * * * * /
static void signal_handler (int dummy) {
    printf ("Идентификатор потока, обрабатывающего сигнал: %lx\n",
                pthread_self ());
}

/* * * * * * * * * * * * * * * * * * * */
/* Стартовая функция потока управления,    */
/* которому будет направлен сигнал     */
/* * * * * * * * * * * * * * * * * * * */
static void *thread_start (void *dummy) {
    printf ("Идентификатор нового потока управления: %lx\n", 
                pthread_self ());
    while (1) {
        sleep (1);
    }

    return (NULL);
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Функция main() задает способ обработки сигнала SIGINT,     */
/* создает поток управления и посылает ему сигнал     */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
int main (void) {
    pthread_t thread_id;
    struct sigaction act;

    /* Установим реакцию на сигнал SIGINT */
    act.sa_handler = signal_handler;
    (void) sigemptyset (&act.sa_mask);
    act.sa_flags = 0;
    (void) sigaction (SIGINT, &act, 
            (struct sigaction *) NULL);

    if ((errno = pthread_create (&thread_id, NULL, 
            thread_start, NULL)) != 0) {
        perror ("PTHREAD_CREATE");
        return (errno);
    }
    printf ("Идентификатор созданного потока управления: %lx\n", 
                thread_id);

    (void) pthread_kill (thread_id, SIGINT);
    printf ("После вызова pthread_kill()\n");

    sleep (1);
    printf ("Выспались...\n");

    return (0);
}
Листинг 1.33. Пример использования механизма сигналов в многопотоковой программе.
Идентификатор созданного потока управления: 402 
    После вызова pthread_kill() 
    Идентификатор потока, обрабатывающего сигнал: 402 
    Идентификатор нового потока управления: 402 
    Выспались...
Листинг 1.34. Возможные результаты работы многопотоковой программы, использующей механизм сигналов.

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

Читателю предлагается самостоятельно проанализировать, как будет вести себя приведенная программа при подразумеваемом способе обработки сигнала SIGINT.

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

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа демонстрирует взаимодействие сигналов     */
/* и ожидания завершения потока управления     */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <stdio.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>

/* * * * * * * * * * * * * * */
/* Функция обработки сигнала  */
/* * * * * * * * * * * * * * */
static void signal_handler (int dummy) {
    printf ("Идентификатор потока, обрабатывающего сигнал: %lx\n",
                 pthread_self ());
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Стартовая функция создаваемого потока управления     */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
static void *thread_start (void *thread_id) {
    printf ("Идентификатор нового потока управления: %lx\n", 
                pthread_self ());
    (void) pthread_kill ((pthread_t) thread_id, SIGINT);

    return ((void *) pthread_self ());
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Функция main() задает способ обработки сигнала SIGINT, */
/* создает поток управления и ожидает его завершения     */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
int main (void) {
    pthread_t thread_id;
    struct sigaction act;
    void *pv;

    /* Установим реакцию на сигнал SIGINT */
    act.sa_handler = signal_handler;
    (void) sigemptyset (&act.sa_mask);
    act.sa_flags = 0;
    (void) sigaction (SIGINT, &act, (struct sigaction *) NULL);

    if ((errno = pthread_create (&thread_id, NULL, 
            thread_start, (void *) pthread_self ())) != 0) {
        perror ("PTHREAD_CREATE");
        return (errno);
    }
    printf ("Идентификаторы начального и созданного потоков "
                "управления: " "%lx %lx\n", pthread_self (), 
                thread_id);

    /* Дождемся завершения созданного потока управления */
    if ((errno = pthread_join (thread_id, &pv)) != 0) {
        perror ("PTHREAD_JOIN");
        return (errno);
    }
    printf ("Статус завершения созданного потока "
                "управления: %p\n", pv);

    return (0);
}
Листинг 1.35. Пример программы, обрабатывающей сигнал во время ожидания завершения потока управления.

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

Идентификаторы начального и созданного потоков управления:     400 402 
Идентификатор нового потока управления:     402 
Идентификатор потока, обрабатывающего сигнал:     400 
Статус завершения созданного потока управления:     0x402
Листинг 1.36. Возможные результаты работы программы, обрабатывающей сигнал во время ожидания завершения потока управления.

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

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