[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]

Программирование: Разработка и отладка программ


Завершение работы нитей

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

Более подробно завершение работы нитей описывается в следующих разделах:

Нормальное завершение работы нити

Процесс может завершить свою работу в любой момент и из любой нити с помощью процедуры exit. Аналогично, нить может завершить свою работу в любой момент вызовом процедуры pthread_exit.

Вызов процедуры exit приводит к завершению работы процесса в целом, включая все его нити. В программах с несколькими нитями процедуру exit следует вызывать только в том случае, когда необходимо завершить весь процесс: например, при возникновении неисправимой ошибки. Вместо этой процедуры следует пользоваться процедурой pthread_exit, в том числе и для завершения главной нити.

Вызов процедуры pthread_exit приводит к завершению нити, вызвавшей эту процедуру. Параметр status при этом сохраняется библиотекой; позднее можно получить доступ к этому параметру при стыковке (Стыковка нитей) с завершенной нитью. Вызов процедуры pthread_exit похож на возврат из главной процедуры нити, с некоторыми отличиями. Результат возврата из главной процедуры нити зависит от типа нити:

Для предотвращения неявного вызова процедуры exit рекомендуется всегда завершать работу нити вызовом pthread_exit.

Естественное завершение главной нити (например, путем вызова процедуры pthread_exit из процедуры main) не приводит к завершению процесса. Завершается только главная нить. Если главная нить завершила работу, то процесс завершится при завершении работы его последней нити. В этом случае код возврата процесса (обычно это значение, возвращаемое процедурой main, или параметр процедуры exit) равен 0, если последняя нить была уничтожена, и 1 в противном случае.

Приведенный ниже пример представляет собой несколько измененный вариант первой программы с несколькими нитями. Программа показывает в точности 10 сообщений на каждом языке. Для этого после создания двух нитей в процедуре main вызывается процедура pthread_exit, а в процедуре Thread предусмотрен цикл.

#include <pthread.h>    /* первый включаемый файл - pthread.h */
#include <stdio.h>      /* поддержка функции printf()         */

void *Thread(void *string)
{ 
      int i; 
      for (i=0; i<10; i++) 
              printf("%s\n", (char *)string); 
      pthread_exit(NULL);
}

int main()
{
        char *e_str = "Hello!";
        char *f_str = "Bonjour !";
 
        pthread_t e_th;
        pthread_t f_th;
 
        int rc;
 
        rc = pthread_create(&e_th, NULL, Thread, (void *)e_str);
        if (rc)
                exit(-1);
        rc = pthread_create(&f_th, NULL, Thread, (void *)f_str);
        if (rc)
                exit(-1);
        pthread_exit(NULL);
}

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

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

Принудительное завершение работы нити

Механизм принудительного завершения работы позволяет нити завершить любую другую нить процесса в управляемом режиме. Целевая нить (т.е. нить, которую следует завершить) может несколькими способами отложить запрос на завершение, выполнив сначала необходимую процедуру очистки. При принудительном завершении работы нити она неявно вызывает процедуру pthread_exit((void *)-1).

Для генерации запроса на принудительное завершение нити вызывается процедура pthread_cancel. При возврате из этой процедуры запрос регистрируется, но целевая нить может еще выполняться. Вызов процедуры pthread_cancel заканчивается неудачей только в случае, если указан неверный ИД нити.

Запрет и режим принудительного завершения

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

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

Запрет завершения Режим завершения Вариант
Запрещено Любой (параметр игнорируется) Вариант 1
Разрешено Отложенное Вариант 2
Разрешено Асинхронное Вариант 3

Ниже подробно рассматриваются все три варианта

  1. Принудительное завершение запрещено. Любой запрос на принудительное завершение переводится в ожидающее состояние до тех пор, пока принудительное завершение не будет разрешено, или пока обработка нити не завершится другим путем.

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

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

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

  3. Асинхронное принудительное завершение. Запросы на принудительное завершение выполняются немедленно.

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

Поддержка асинхронного завершения

Функция называется поддерживающей асинхронное завершение, если ее асинхронное завершение на любой инструкции не приводит к повреждению каких-либо ресурсов.

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

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

Три перечисленные ниже процедуры устойчивы к асинхронному завершению: при их принудительном завершении, даже асинхронном, ресурсы будут правильно освобождены.

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

Точки завершения

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

Создать явную точку завершения можно также с помощью процедуры pthread_testcancel. Эта процедура просто создает точку завершения. Если разрешено отложенное завершение, то при наличии ожидающих запросов на принудительное завершение в этой точке они будут выполнены, и нить завершится. В противном случае обработка нити будет продолжена.

Точки вызова следующих процедур представляют собой неявные точки завершения:

Процедуры pthread_mutex_lock и pthread_mutex_trylock не образуют неявные точки завершения, так как в противном случае неявные точки завершения создавались бы во всех функциях, вызывающих эти процедуры, и число точек завершения было бы непомерно велико, что сильно затруднило бы процесс программирования. Нужно было бы либо многократно запрещать и разрешать принудительное завершение, либо везде добавлять процедуры очистки. Дополнительная информация об указанных процедурах приведена в разделе Использование взаимных блокировок.

Точки завершения

Точки завершения образуются при выполнении следующих функций внутри нити:

aio_suspend close
creat fcntl
fsync getmsg
getpmsg lockf
mq_receive mq_send
msgrcv msgsnd
msync nanosleep
open pause
poll pread
pthread_cond_timedwait pthread_cond_wait
pthread_join pthread_testcancel
putpmsg pwrite
read readv
select sem_wait
sigpause sigsuspend
sigtimedwait sigwait
sigwaitinfo sleep
system tcdrain
usleep wait
wait3 waitid
waitpid write
writev

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

catclose catgets catopen
closedir closelog ctermid
dbm_close dbm_delete dbm_fetch
dbm_nextkey dbm_open dbm_store
dlclose dlopen endgrent
endpwent fwprintf fwrite
fwscanf getc getc_unlocked
getchar getchar_unlocked getcwd
getdate getgrent getgrgid
getgrgid_r getgrnam getgrnam_r
getlogin getlogin_r popen
printf putc putc_unlocked
putchar putchar_unlocked puts
pututxline putw putwc
putwchar readdir readdir_r
remove rename rewind
endutxent fclose fcntl
fflush fgetc fgetpos
fgets fgetwc fgetws
fopen fprintf fputc
fputs getpwent getpwnam
getpwnam_r getpwuid getpwuid_r
gets getutxent getutxid
getutxline getw getwc
getwchar getwd rewinddir
scanf seekdir semop
setgrent setpwent setutxent
strerror syslog tmpfile
tmpnam ttyname ttyname_r
fputwc fputws fread
freopen fscanf fseek
fseeko fsetpos ftell
ftello ftw glob
iconv_close iconv_open ioctl
lseek mkstemp nftw
opendir openlog pclose
perror ungetc ungetwc
unlink vfprintf vfwprintf
vprintf vwprintf wprintf
wscanf

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

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

Пример принудительного завершения нити

Приведенный ниже пример представляет собой вариант первой программы с несколькими нитями. Обе нити "writer" принудительно завершаются через 10 секунд после начала выполнения, если каждая из них напечатала свое сообщение хотя бы 5 раз.

#include <pthread.h>    /* первый включаемый файл - pthread.h */
#include <stdio.h>      /* поддержка функции printf() */
#include <unistd.h>     /* поддержка функции sleep()          */

void *Thread(void *string) {
      int i;
      int o_state;
      /* запрет принудительного завершения */
      pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &o_state);
      /* вывод пяти сообщений */
      for (i=0; i<5; i++)
              printf("%s\n", (char *)string);
      /* разрешение принудительного завершения */
      pthread_setcancelstate(o_state, &o_state);
      /* продолжение вывода сообщений */
      while (1)
              printf("%s\n", (char *)string);
      pthread_exit(NULL); }

int main()
{
        char *e_str = "Hello!";
        char *f_str = "Bonjour !";
 
        pthread_t e_th;
        pthread_t f_th;
 
        int rc;
 
        /* создание обеих нитей */
        rc = pthread_create(&e_th, NULL, Thread, (void *)e_str);
        if (rc)
                return -1;
        rc = pthread_create(&f_th, NULL, Thread, (void *)f_str);
        if (rc)
                return -1;
  

        /* ожидание */
        sleep(10);
 
        /* запрос на принудительное завершение */
        pthread_cancel(e_th);
        pthread_cancel(f_th);
 
        /* ожидание */
        sleep(10);
        pthread_exit(NULL);
}

Процедуры очистки

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

Вызов процедур очистки

Процедуры очистки создаются для каждой нити отдельно. Если в одной нити есть несколько процедур очистки, то они помещаются в стек процедур очистки для данной нити. Вызов процедур очистки происходит в следующих случаях:

Процедура очистки помещается в стек очистки процедурой pthread_cleanup_push. Процедура pthread_cleanup_pop выталкивает из стека самую верхнюю процедуру очистки и, возможно, выполняет ее. Эту процедуру применяют также для удаления ненужных процедур очистки.

Процедура очистки представляет собой пользовательскую процедуру с одним параметром типа "указатель на void", передаваемым при вызове процедуры pthread_cleanup_push. Для того чтобы процедура очистки освободила область данных, передайте указатель на эту область.

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

/* процедура очистки */
 
cleaner(void *buffer)
 
{
        free(buffer);
}

/* фрагмент другой процедуры */
...
myBuf = malloc(1000);
if (myBuf != NULL) {
        
        pthread_cleanup_push(cleaner, myBuf);
 
        /*
         *       выполняются любые операции с буфером,
         *       в том числе вызовы других функций
         *       и точек завершения
         */
        
        /* выталкивание процедуры очистки и освобождение буфера за 1 вызов */
        pthread_cleanup_pop(1);
}

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

Согласование операций помещения в стек и выталкивания из стека

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

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

#define pthread_cleanup_push(rtm,arg) { \
         /* другие операторы */

Процедура pthread_cleanup_pop реализована в виде закрывающей скобки, за которой следует последовательность операторов:

#define pthread_cleanup_pop(ex) \
         /* другие операторы */
}

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

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

Связанная информация

Основные сведения о нитях

Обзор основных операций с нитями

Создание нитей

Список основных функций работы с нитями


[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]