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

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

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

Еще одна полезная операция, связанная с обработкой завершения потока управления, – его динамическое обособление, выполняемое функцией pthread_detach() (см. листинг 1.37).

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

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

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

Если приложение заботится об аккуратном освобождении памяти, то для всех потоков управления, созданных с атрибутом PTHREAD_CREATE_JOINABLE, следует предусмотреть вызов либо pthread_join(), либо pthread_detach().

В качестве примера многопотоковой программы приведем серверную часть рассматривавшегося в курсе [1] приложения, копирующего строки со стандартного ввода на стандартный вывод с "прокачиванием" их через потоковые сокеты (см. листинг 1.38).

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа процесса (будем называть его серверным),     */
/* принимающего запросы на установления соединения и     */
/* запускающего потоки управления для их обслуживания     */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <stdio.h>
#include <netdb.h>
#include <pthread.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Стартовая (и единственная) функция потоков управления, */
/* обслуживающих запросы на копирование строк,     */
/* поступающих из сокета         */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
void *srv_thread_start (void *ad) {
    FILE *fpad;         /* Поток данных, соответствующий */
                            /* дескриптору ad     */
    char line [LINE_MAX]; /* Буфер для принимаемых строк */
            /* Структура для записи адреса */
    struct sockaddr_in sai;
            /* Длина адреса */
    socklen_t sai_len = sizeof (struct sockaddr_in);

    /* Опросим адрес партнера по общению (передающего сокета) */
    if (getpeername ((int) ad, (struct sockaddr *) &sai, 
            &sai_len) < 0) {
        perror ("GETPEERNAME");
        return (NULL);
    }

    /* По файловому дескриптору ad сформируем */
    /* буферизованный поток данных     */
    if ((fpad = fdopen ((int) ad, "r")) == NULL) {
        perror ("FDOPEN");
        return (NULL);
    }

    /* Цикл чтения строк из сокета     */
    /* и выдачи их на стандартный вывод     */
    while (fgets (line, sizeof (line), fpad) != NULL) {
        printf ("Вы ввели и отправили с адреса %s, "
                    "порт %d :", inet_ntoa (sai.sin_addr), 
                    ntohs (sai.sin_port));
        fputs (line, stdout);
    }

    /* Закрытие соединения */
    shutdown ((int) ad, SHUT_RD);
    (void) fclose (fpad);

    return (NULL);
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* В функции main() принимаются запросы на установление */
/* соединения и запускаются потоки управления для их обслуживания */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
int main (void) {
    int sd;                     /* Дескриптор слушающего сокета */
    int ad;                     /* Дескриптор приемного сокета     */
            /* Буфер для принимаемых строк */
    struct addrinfo hints = {AI_PASSIVE, AF_INET, 
                SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL, NULL};
        /* Указатель – выходной аргумент getaddrinfo */
    struct addrinfo *addr_res;
    int res;              /* Результат getaddrinfo     */
    pthread_attr_t patob; /* Атрибутный объект для создания    */
                          /* потоков управления */
    pthread_t adt_id;     /* Идентификатор обслуживающего потока управления */

    /* Создадим слушающий сокет */
    if ((sd = socket (AF_INET, SOCK_STREAM, 
            IPPROTO_TCP)) < 0) {
        perror ("SOCKET");
        return (1);
    }

    /* Привяжем этот сокет к адресу сервиса spooler */
    /* на локальном хосте         */
    if ((res = getaddrinfo (NULL, "spooler", &hints, 
                            &addr_res)) != 0) {
        fprintf (stderr, "GETADDRINFO: %s\n", 
                    gai_strerror (res));
        return (2);
    }
    if (bind (sd, addr_res->ai_addr, 
            addr_res->ai_addrlen) < 0) {
        perror ("BIND");
        return (3);
    }

    /* Можно освободить память, которую запрашивала */
    /* функция getaddrinfo()     */
    freeaddrinfo (addr_res);

    /* Пометим сокет как слушающий */
    if (listen (sd, SOMAXCONN) < 0) {
        perror ("LISTEN");
        return (4);
    }

    /* Инициализируем атрибутный объект потоков управления */
    if ((errno = pthread_attr_init (&patob)) != 0) {
        perror ("PTHREAD_ATTR_INIT");
        return (errno);
    }
    /* Потоки управления будем создавать обособленными */
    (void) pthread_attr_setdetachstate (&patob, 
        PTHREAD_CREATE_DETACHED);

    /* Цикл приема соединений и запуска     */
    /* обслуживающих потоков управления     */
        while (1) {
        /* Примем соединение. */
        /* Адрес партнера по общению нас     */
        /* в данном случае не интересует     */
        if ((ad = accept (sd, NULL, NULL)) < 0) {
            perror ("ACCEPT");
            return (6);
        }

        /* Запустим обслуживающий поток управления */
        if ((errno = pthread_create (&adt_id, &patob, 
                srv_thread_start,(void *) ad)) != 0) {
            perror ("PTHREAD_CREATE");
            return (errno);
        }
    }

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

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

Можно надеяться, что и следующая программа (см. листинг 1.39), реализующая идею обработки данных с контролем времени, подтверждает, что применение потоков управления позволяет сделать исходный текст более простым и наглядным по сравнению с "беспотоковым" вариантом (см. курс [1]). Удалось избавиться от такого сугубо "неструктурного" средства, как нелокальные переходы, и от ассоциированных с ними тонкостей, чреватых ошибками.

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа вызывает функции обработки в рамках    */
/* порождаемых потоков управления и контролирует время    */
/* их выполнения  с помощью интервального таймера     */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <signal.h>
#include <errno.h>

/* Период интервального таймера (в секундах) */
#define IT_PERIOD             1

static pthread_t cthread_id; /* Идентификатор текущего */ 
                             /* потока управления, */
                             /* обрабатывающего данные */

static int in_proc_data = 0; /* Признак активности */
                             /* потока обработки данных */

static double s;             /* Результат функций */
                             /* обработки данных */

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Функция обработки срабатывания таймера реального     */
/* времени (сигнал SIGALRM)         */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
static void proc_sigalrm (int dummy) {
    if (in_proc_data) {
        /* Не имеет значения, какой поток обрабатывает сигнал */
        /* и заказывает терминирование (быть может, себя) */
        (void) pthread_cancel (cthread_id);
        }
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Обработчик завершения потока управления.     */
/* Сбрасывает признак активности потока обработки данных */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
static void proc_data_cleanup_handler (void *arg) {
    in_proc_data = (int) arg;
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Стартовая функция потока управления, обрабатывающего     */
/* данные. Аргумент – указатель на функцию обработки данных */    
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
void *start_func (void *proc_data_func) {
    /* Поместим в стек обработчик завершения */
    pthread_cleanup_push (proc_data_cleanup_handler, 0);
    in_proc_data = 1;     /* Время пошло ... */

    /* На время выполнения функции обработки данных установим */
    /* асинхронный тип терминирования, иначе оно не сработает */
    (void) pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, 
            NULL);

        /* Выполним функцию обработки данных */
        ((void (*) (void)) (proc_data_func)) ();

        /* Установим отложенный тип терминирования,     */
        /* иначе изъятие обработчика из стека     */
        /* будет небезопасным действием     */
        (void) pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, 
            NULL);

    /* Выполним обработчик завершения и удалим его из стека */
        pthread_cleanup_pop (1);
        return (NULL);
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Первая функция обработки данных (вычисляет ln (2))  */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
static void proc_data_1 (void) {
    double d = 1;
    int i;

    s = 0;
    for (i = 1; i <= 100000000; i++) {
        s += d / i;
        d = -d;
    }
}

/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Вторая функция обработки данных (вычисляет sqrt (2))*/
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
static void proc_data_2 (void) {
    s = 1;
    do {
        s = (s + 2 / s) * 0.5;
    } while ((s * s – 2) > 0.000000001);
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Функция main() задает способ обработки сигнала SIGALRM, */
/* взводит периодический таймер реального времени     */
/* и запускает в цикле потоки обработки данных     */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
        int main (void) {
    /* Массив указателей на функции обработки данных */
    void (*fptrs []) (void) = {proc_data_1, 
                               proc_data_2, NULL};
                               /* Указатель на указатель на */
                               /* текущую функцию обработки данных */
    void (**tfptr) (void);
    void *pstat;               /* Статус завершения потока */
                               /* обработки данных */
    struct itimerval itvl;
    struct sigaction sact;
    int i;

    /* Установим реакцию на сигнал SIGALRM */
    sact.sa_handler = proc_sigalrm;
    sact.sa_flags = 0;
    (void) sigemptyset (&sact.sa_mask);
    if (sigaction (SIGALRM, &sact, NULL) < 0) {
        perror ("SIGACTION");
        return (1);
    }

    /* Сделаем таймер реального времени периодическим */
    itvl.it_interval.tv_sec = IT_PERIOD;
    itvl.it_interval.tv_usec = 0;

    /* Цикл запуска потоков обработки данных.     */
    /* Выполним его дважды         */
    for (i = 0; i < 2; i++) {
        for (tfptr = fptrs; *tfptr != NULL; tfptr++) {
            /* Взведем интервальный таймер реального времени */
            itvl.it_value.tv_sec = IT_PERIOD;
            itvl.it_value.tv_usec = 0;
            if (setitimer (ITIMER_REAL, &itvl, NULL) < 0) {
                perror ("SETITIMER");
                return (2);
            }

            /* Создадим поток обработки данных,     */
            /* затем дождемся его завершения     */
            if ((errno = pthread_create (&cthread_id, NULL, 
                    start_func, (void *) *tfptr)) != 0) {
                perror ("PTHREAD_CREATE");
                return (errno);
            }
            if ((errno = pthread_join (cthread_id, 
                    &pstat)) != 0) {
                perror ("PTHREAD_JOIN");
                return (errno);
            }

            if (pstat == PTHREAD_CANCELED) {
                printf ("Частичный результат функции "
                            "обработки данных: %g\n", s);
            } else {
                printf ("Полный результат функции "
                            "обработки данных: %g\n", s);
            }
        }
    }

    return 0;
}
Листинг 1.39. Пример многопотоковой программы, осуществляющей обработку данных с контролем времени.

Возможные результаты работы приведенной программы показаны на листинге 1.40.

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

К сожалению, там, где есть недетерминированность и асинхронность, без тонкостей все равно не обойтись. Мы обратим внимание на три из них. Во-первых, возможно срабатывание таймера и доставка сигнала SIGALRM до того, как завершится (или даже начнется) первый вызов pthread_create() и будет инициализирована переменная cthread_id. Чтобы не допустить терминирования "неопределенного" потока управления в функции обработки сигнала, введен признак активности потока обработки данных. Если он установлен, переменная cthread_id заведомо инициализирована.

Во-вторых, не имеет значения, в контексте какого из потоков выполняется функция обработки сигнала – вполне допустимо, чтобы поток заказал собственное терминирование.

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

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

На листингах 1.41 и 1.42 показана программа, которая в цикле порождает практически пустые процессы и дожидается их завершения; на листинге 1.43 приведены данные о времени ее работы, полученные с помощью команды time -p. Даже если сделать процессам послабление и убрать вызов execl(), времена получатся довольно большими (см. листинг 1.44) в сравнении с аналогичными данными для варианта с потоками управления (см. листинги 1.45 и 1.46).

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>

#define N 10000

int main (void) {
  int i;

  for (i = 0; i < N; i++) {
    switch (fork ()) {
      case -1:
        perror ("FORK");
        return (1);
      case 0:
        /* Порожденный процесс */
        (void) execl ("./dummy", "dummy",
            (char *) 0);
        exit (0);
        default:
        /* Родительский процесс */
        (void) wait (NULL);
      }
  }

  return 0;
}
Листинг 1.41. Пример программы, порождающей в цикле практически пустые процессы.
int main (void) {
    return 0;
}
Листинг 1.42. Содержимое файла dummy.c
real 34.97
user 12.36
sys 22.61
Листинг 1.43. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы (вариант с вызовом execl()).
real 11.49
user 2.38
sys 9.11
Листинг 1.44. Возможные результаты измерения времени работы программы, порождающей в цикле практически пустые процессы (вариант без вызова execl()).
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>

#define N 10000

static void *thread_start (void *arg) {
    pthread_exit (arg);
}

int main (void) {
    pthread_t thread_id;
    int i;

    for (i = 0; i < N; i++) {
        if ((errno = pthread_create (
             &thread_id, NULL, 
             thread_start, NULL)) != 0) {
            perror ("PTHREAD_CREATE");
            return (errno);
        }
        if ((errno = pthread_join (
             thread_id, NULL)) != 0) {
            perror ("PTHREAD_JOIN");
            return (errno);
        }
    }

    return (0);
}
Листинг 1.45. Пример программы, порождающей в цикле потоки управления.
real 2.08
user 0.52
sys 1.56
Листинг 1.46. Возможные результаты измерения времени работы программы, порождающей в цикле потоки управления.

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

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

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