Создание и завершение нитей
4. Принудительное завершение нити (прерывание)
Библиотечная функция pthread_cancel(3C) принудительно завершает нить. В зависимости от свойств нити и некоторых других обстоятельств, нить может продолжать исполнение некоторое время после вызова pthread_cancel(3C).
Нить может установить одну или несколько функций-обработчиков, которые будут вызваны при попытке прервать исполнение этой нити. Такие обработчики могли бы освободить дополнительные ресурсы, занятые нитью, например блоки динамической памяти или мутексы, или привести в согласованноесостояние разделяемые структуры данных.
Момент, в который нить получает сообщение о попытке прервать ее исполнение функцией pthread_cancel(3C) контролируется атрибутами нити, известными как cancel state и cancel type.
Cancel state (состояние прерывания) определяет, разрешено ли прерывание нити как таковое. Т.е. этот атрибут может иметь два значения – разрешено или запрещено. Если прерывание разрешено, нить немедленно получает сообщение о попытке ее прервать (хотя, в зависимости от cancel type, может отреагировать на это сообщение лишь через некоторое время). Если прерывание запрещено, попытки прерывания нити накапливаются. После того, как прерывания все-таки разрешат, нить получит сигналы о накопившихся попытках.
Переключение состояния прерывания осуществляется функцией pthread_setcancelstate(3C).
Первый параметр этой функции входной и может принимать значения PTHREAD_CANCEL_ENABLE (прерывание разрешено) и PTHREAD_CANCEL_DISABLE (прерывание запрещено). Эти значения – препроцессорные макроопределения, содержащиеся в файле pthread.h. Вызов функции с другими значениями первого параметра приведет к ошибке EINVAL. Второй параметр функции – выходной, содержит указатель на переменную, в которой будет размещено старое значение типа прерывания. В качестве этого указателя можно передать NULL, в этом случае старое значение состояния будет потеряно. По умолчанию, нить создается с разрешенными прерываниями.
Cancel type (тип прерывания) определяет, в какие моменты нить проверяет сообщения о прерываниях. Этот атрибут может принимать два значения – PTHREAD_CANCEL_DEFERRED (отложенное прерывание) и PTHREAD_CANCEL_ASYNCHRONOUS (асинхронное прерывание). По умолчанию, нить создается с отложенным типом прерываний. Что означает каждое из возможных значений этого атрибута, описывается далее в этом разделе.
Установка типа прерывания осуществляется функцией pthread_setcanceltype(3C). Схема передачи параметров этой функции аналогична pthread_setcancelstate(3C).
Тип и состояние прерывания могут быть заданы в момент создания нити при помощи установки соответствующих полей в структуре pthread_attr_t. Обратите внимание, что оба эти атрибута задаются либо в момент создания нити, либо самой нитью. Внешними по отношению к нити средствами их изменить невозможно. Поэтому проверка значений этих атрибутов в определенные моменты времени не может приводить к ошибкам соревнования.
Асинхронное прерывание означает, что библиотека прерывает нить как можно скорее (хотя во многих ситуациях не удается гарантировать, чтобы это происходило точно в тот момент, когда другая нить вызвала pthread_cancel(3С) ). Асинхронное прерывание требует тщательного анализа всех возможных моментов, когда оно может произойти, и обработки всех ситуаций, связанных с прерываниями в неудачные моменты. Так, если прерывание произойдет во время работы с библиотекой, которая не считается thread-safe, внутренние данные этой библиотеки могут остаться в несогласованном состоянии.
Для корректной работы большинства библиотек в таких условиях простой поддержки многопоточности не достаточно. В страницах системного руководства Solaris уровень поддержки многопоточности описывается несколькими типами атрибутов, которые рассматриваются в "лекции 5" ; безопасность библиотеки или функции при использовании асинронных прерываний описывается атрибутом MT-Level. У библиотеки, которая безопасна для применения в таких условиях, этот атрибут имеет значение Asynchronous-Cancel-Safe. Для всех библиотек и функций, для которых этот атрибут явным образом не указан на соответствующей странице системного руководства, следует предполагать, что они небезопасны для использования в режиме асинхронных прерываний.
Отложенное прерывание означает, что нить получает сообщение о прерывании лишь в определенные моменты, известные как точки прерывания (cancellation point). Эти точки, в свою очередь, делятся на две категории – явные и неявные. Явные точки прерывания – это вызовы функции pthread_testcancel(3C). Неявные точки прерывания – это вызовы следующих функций и системных вызовов:
aio_suspend(3RT), close(2), creat(2), getmsg(2), getpmsg(2), lockf(3C), mq_receive(3RT), mq_send(3RT), msgrcv(2), msgsnd(2), msync(3C), nanosleep(3RT), open(2), pause(2), poll(2), pread(2), pthread_cond_timedwait(3C), pthread_cond_wait(3C), pthread_join(3C), pthread_testcancel(3C), putmsg(2), putpmsg(2), pwrite(2), read(2), readv(2), select(3C), sem_wait(3RT), sigpause(3C), sigwaitinfo(3RT), sigsuspend(2), sigtimedwait(3RT), sigwait(2), sleep(3C), sync(2), system(3C), tcdrain(3C), usleep(3C), wait(3C), waitid(2), wait3(3C), waitpid(3C), write(2), writev(2), fcntl(2) (с командой F_SETLKW).
Список приведен для Solaris 10. Получить полный список неявных точек прерывания для вашей версии ОС можно получить на странице руководства cancellation(5). Несколько забегая вперед, необходимо отметить, что функция pthread_mutex_lock(3С) не является неявной точкой прерывания, и в соответствии с требованиями стандарта POSIX, не должна являться таковой.
Стандарт POSIX допускает наличие неявных точек прерывания в некоторых других системных вызовах и библиотечных функциях, которые перечислены на странице http://www.opengroup.org/onlinepubs/007908799/xsh/threads.html. Слушатели, знакомые с реализацией стандартных библиотек языка C должны заметить, что это преимущественно функции, которые содержат или могут содержать вызовы неявных точек прерывания, перечисленных выше. В частности, это большинство функций буферизованного ввода-вывода ( printf(3C), fread(3C), fwrite(3C) ).
Однако реализации POSIX Thread Library в Solaris 10 и Linux 2.6 не содержат неявных точек прерывания ни в одной из этих функций. Во всяком случае, в Solaris 10 список функций, содержащих неявные точки прерывания, приведенный на странице руководства cancellation(5), является исчерпывающим.
Слушателям, интересующимся вопросом, как удалось реализовать printf(3C) или fwrite(3C) без обращения к точке прерывания в теле write(2), я рекомендую обратиться к исходным текстам библиотеки языка C, размещенным на сайте http://www.opensolaris.org, или просто пройти в пошаговом отладчике код библиотечной функции fwrite(3C). Для этого предварительно рекомендуется выключить буферизацию функцией setvbuf(3C), тогда каждый вызов fwrite будет вызывать соответствующий системный вызов лишь после небольшого количества проверок, и будет легче понять логику кода.
Более сложный вопрос – зачем это было сделано. Ответ, скорее всего, состоит в том, что функции буферизованного ввода-вывода – не просто "обертки" над соответствующими системными вызовами, они осуществляют определенные (и иногда довольно сложные) операции над структурой данных, которая, собственно, и является буфером ввода-вывода. Прерывание этих библиотечных функций в неудачный момент либо приводило бы к риску оставить буфер ввода-вывода в несогласованном состоянии, либо требовало бы сложных вычислений для отслеживания и восстановления его согласованности.
6. Обработка прерывания нити
Нить может установить одну или несколько функций, которые будут вызываться при получении ей сообщения о прерываниии. Эти функции могут восстанавливать согласованность структур данных, с которыми нить работала до получения прерывания, освобождать мутексы и другие примитивы синхронизации, занятые нитью, освобождать динамическую память. Функция-обработчик прерывания устанавливается макрокомандой
pthread_cleanup_push(3C),
которая определена в файле /usr/include/pthread.h. Эта макрокоманда внешне выглядит как вызов одноименной функции с двумя параметрами – указателем на функцию-обработчик и значением, которое следует передать этой функции в качестве параметра.
Функции-обработчики вызываются в стековом порядке (Last In First Out), т.е. в порядке, обратном тому, в котором они устанавливались, и из того контекста, в котором они устанавливались. Иными словами, в качестве параметров функций-обработчиков можно использовать указатели на локальные переменные, определенные в тех блоках, в которых стоял вызов соответствующего pthread_cleanup_push(3C).
Каждый вызов pthread_cleanup_push(3C) должен сопровождаться соответствующим вызовом парной макрокоманды pthread_cleanup_pop(3C). Эта макрокоманда должна вызываться в пределах того же блока, в котором была вызвана соответствующая pthread_cleanup_push(3C).
Нарушение этого требования приведет к ошибке компиляции. В действительности, макрокоманды pthread_cleanup_push/pop содержат, соответственно, открывающую и закрывающую фигурную скобки. Это не является требованием стандарта POSIX, но поможет понять, к каким именно ошибкам компиляции может привести их неправильное использование.
Макроопределение pthread_cleanup_pop(3С) содержит параметр, который указывает, следует ли вызвать обработчик при его выталкивании из стека или нет. Если обработчик осуществляет какие-то сложные неатомарные операции, рекомендуется его вызывать, а не дублировать соответствующий код в теле основной функции нити.
Не следует использовать в функциях-обработчиках такие функции, как lonjmp(3C)/siglongjmp(3C) или оператор throw языка С++, это может привести, и в большинстве реализаций действительно приведет, к нарушению порядка вызова других обработчиков или даже к разрушению стека и аварийному завершению программы. В большинстве реализаций другие обработчики просто не будут вызваны.