В ходе этого раздела вы изучите
- Создание нитей с атрибутами по умолчанию
- Передачу параметров нити
- Завершение нити
- Ожидание завершения другой нити
- Принудительное завершение нити
- Обработку принудительного завершения нити
Создание нитей с атрибутами по умолчанию
В POSIX Thread API нить создается библиотечной функцией pthread_create(3C).
Параметры этой функции:
pthread_t * thread – Выходной параметр. Указатель на переменную, в которой при успешном завершении будет размещен идентификатор нити.
const pthread_attr_t * attr – Входной параметр. Указатель на структуру, в которой заданы атрибуты нити (рассматривается на следующей лекции). Если этот указатель равен NULL, используются атрибуты по умолчанию.
void *(*start_routine)(void*) – Входной параметр. Указатель на функцию, которая будет запущена во вновь созданной нити.
void * arg – Входной параметр. Значение, которое будет передано в качестве параметра start_routine.
Возвращаемое значение
0 при успешном завершении
Код ошибки при неудачном завершении
Большинство других функций POSIX Threads API используют аналогичное соглашение о кодах возврата. Если в нашем учебном пособии у функции не указано описание кода возврата, значит, что она возвращает 0 при успешном завершении и код ошибки при ошибке. В страницах man(1) всегда указывается точное описание кода возврата всех функций.
Коды ошибок
Значения кодов ошибок определены в виде символов препроцессора в файле errno.h
EAGAIN – системе не хватает ресурсов для создания нити. Возможно, не хватает памяти под стек, исчерпан архитектурный лимит на количество нитей в процессе (PTHREAD_THREADS_MAX) либо административное ограничение на количество нитей. Как и у остальных системных вызовов, код EAGAIN означает, что повторный вызов функции с теми же параметрами может не привести к ошибке.
EINVAL – один из параметров имеет недопустимое значение. Например, указатель на start_routine указывает на страницу памяти, исполнение которой запрещено.
EPERM – вы не имеете полномочий для исполнения нити с заданными атрибутами. Например, вы не можете установить заданные в структуре attr класс планирования или приоритет.
Пример 1
Важное замечание
Параметр функции нити описан как void *, но библиотека никогда не пытается обращаться к нему как к указателю. Поэтому этот указатель можно использовать либо как ссылку на структуру (блок параметров нити), либо для передачи скалярного значения.
При передаче структур данных в качестве параметра нужно проявлять осторожность.
Во первых, не следует передавать структуры данных, размещенные в стеке родительской нити, то есть переменные с классом памяти auto и блоки памяти, размещенные при помощи alloca(3C). Действительно, если ваша нить вернет управление из текущей функции или вообще завершится до того, как созданная нить начнет работать с блоком параметров, получится, что вы передали в качестве параметра висячую ссылку.
Во вторых, при передаче структур данных, размещенных при помощи malloc(3C) или оператора new языка C++, необходимо решить вопрос о том, кто будет освобождать эту структуру. Если структура не будет освобождена при помощи free(3C) или оператора delete, это приведет к утечке памяти. Существует несколько решений этого вопроса, приемлемых в разных ситуациях.
Одно из решений состоит в том, что родитель размещает структуру, а созданная нить ее освобождает. Но обычно это считается дурным тоном при программировании на C/C++ (хорошим тоном считается, чтобы вызываемая функция не знала, каким образом выделена память под переданные ей параметры).
Другое решение состоит в том, что родитель размещает структуру, дожидается завершения потомка при помощи вызова pthread_join(3C) и освобождает структуру. В рамках этого подхода можно передавать как структуры данных, созданные при помощи malloc(3C), так и структуры данных, размещенные в стеке родителя. Проблема этого подхода состоит в том, что если родитель будет принудительно завершен при помощи pthread_cancel(3С), он может не дождаться завершения своих потомков, а это приведет либо к утечке памяти, либо к висячим ссылкам.
В программах с небольшим количеством нитей часто передают в качестве параметра указатели на статические переменные, но в больших программах с большим числом нитей это неприемлемо.
Завершение нити
Для завершения нити используется функция pthread_exit(3C). Эта функция всегда завершается успешно и принимает единственный параметр, указатель на код возврата типа void *. Как и в случае с pthread_create(3C), библиотека никогда не пытается обращаться к значению этого параметра как к указателю, поэтому его можно использовать для передачи скалярных значений.
Другой способ завершения нити – это возврат управления из функции start_routine при помощи оператора return. Поскольку функция start_routine должна возвращать тип void *, все ее операторы return должны возвращать значение. Этот способ практически полностью эквивалентен вызову pthread_exit(3C) с тем же самым значением.
Примечание
При компиляции программ на С++ некоторыми версиями компилятора GCC, при завершении нити вызовом pthread_exit(3C) не вызываются деструкторы локальных переменных. Информацию об этом не всегда удается найти в документации. В действительности это определяется не столько версией компилятора, сколько опциями при сборке компилятора и особенностями C runtime/libpthread.
Так, компилятор Sun Studio 11 вызывает деструкторы, а GCC 3.4.3, входящий в поставку Solaris 10 – не вызывает.
GCC 2.95.4 из поставки Debian Woody не вызывает деструкторы, GCC 3.3.5 из поставки Debian Sarge – вызывает.
Протестировать взаимодействие POSIX Thread API и вашего компилятора C++ можно при помощи программы из примера 2.
Пример 2
Примечание 2
Завершение процесса системным вызовом exit(2) или возвратом из функции main приводит к завершению всех нитей процесса. Это поведение описано в стандарте POSIX, поэтому ОС, которые ведут себя иначе (например, старые версии Linux), не соответствуют стандарту. Если вы хотите, чтобы нити вашей программы продолжали исполнение после завершения main, следует завершать main при помощи вызова pthread_exit(3C).
Поведение деструкторов локальных переменных в нитях при завершении процесса при помощи exit(2) не описано в стандарте. На практике они не вызываются даже для переменных, описанных в основной нити программы, и даже в однопоточных программах (проверялось в Solaris и Linux на всех доступных версиях компиляторов).
В действительности, все еще хуже – при выходе по exit(2) начинают вызываться деструкторы статических и глобальных переменных, но нити продолжают работу – в некоторых случаях это может приводить к аварийному завершению программы. Так, если в программе примера 2 в строке 42 закомментировать pthread_exit, то программа, собранная Sun Studio 11, с высокой вероятностью завершается по Segmentation Fault (попытка вывода в std::cout после того, как деструктор std::cout уже отработал). Поэтому если вы хотите экстренно завершить многопоточную программу, написанную на C++, используйте _exit(2). Если вам необходимо обеспечить вызов деструкторов локальных переменных, следует использовать исключения С++.
Примечание 3
К передаче выходного значения нити относятся все те же самые соображения, что и к передаче параметров нити. Хотя выходное значение нити описано как void *, библиотека никогда не обращается к нему как к указателю, поэтому в качестве значения можно возвращать ноль или другие скалярные значения.
При передаче указателей возникают те же проблемы, что и при передаче параметров – опасность возникновения висячих ссылок и опасность утечки памяти. Ни в коем случае нельзя возвращать указатели на локальные переменные и другие объекты, созданные в стеке нити, потому что нить, получающая возвращаемое значение функцией pthread_join(3C) (рассматривается ниже), получает доступ к значению лишь после того, как стек нити и другие ресурсы, связанные с нитью, в том числе thread local data, уже уничтожены.
При использовании для размещения возвращаемого значения malloc(3C) возникает опасность утечки памяти. Действительно, если для размещения значения использовать malloc(3C), то нить, получающая наше значение, должна его освободить. Но если эта нить получит сигнал принудительного завершения (pthread_cancel(3C)), то она не дождется нашего завершения и не освободит наше значение.
Универсального решения этой проблемы не существует. В программах с фиксированным или ограниченным числом нитей часто используют для передачи параметров и выходных значений указатели на статические переменные. В программах с переменным числом нитей обычно используют структуру, когда одна нить создает все остальные нити, размещает для них блоки переменных состояния, дожидается завершения каждой из нитей и уничтожает или переиспользует эти блоки. При этом, разумеется, логично, чтобы нить получала указатель на свой блок переменных состояния как параметр, работала с ним в течении всего срока жизни и возвращала этот же блок в качестве параметра pthread_exit(3C). Но эта архитектура пригодна не для всех приложений.
Ожидание завершения нити
Для ожидания завершения нити и получения ее кода возврата используется библиотечная функция pthread_join(3C). Эта функция имеет два параметра, идентификатор нити типа pthread_t, и указатель на переменную типа void *, в которой размещается значение кода возврата. Если в качестве второго параметра передать нулевой указатель, код возврата игнорируется.
Если требуемая нить еще не завершилась, то нить, сделавшая вызов pthread_join(3С), блокируется. Если такой нити (уже) не существует, pthread_join(3C) возвращает ошибку ESRCH.
Когда нить завершается, то связанные с ней ресурсы существуют до того момента, пока какая-то другая нить не вызовет pthread_join(3C). Однако к тому моменту, кода pthread_join завершается, все ресурсы, занятые нитью (стек, thread local data, дескриптор нити) уничтожаются.
В отличие от процессов Unix, где системный вызов wait(2) может использовать только родитель по отношению к потомкам, любая нить может ждать завершения любой другой нити того же процесса. Если несколько нитей ждут завершения одной и той же нити, которая еще не завершилась, все эти нити блокируются. Однако при завершении нити одна из ожидавших нитей получает код возврата, а остальные ошибку ESRCH.
Если нить пытается ожидать сама себя, она получает ошибку EDEADLK.
Еще одна важная функция, связанная с ожиданием завершения нити – это функция pthread_detach(3C). Эта функция указывает, что все ресурсы, связанные с нитью, необходимо уничтожать сразу после завершения этой нити. При этом уничтожается и код возврата такой нити – при попытке сделать pthread_join(3C) на нить, над которой перед этим сделали pthread_detach(3C), возвращается код ошибки EINVAL.
В руководстве по pthread_detach(3C) в системе Solaris 10 сказано, что главное применение pthread_detach(3C) – это ситуация, когда родитель, ожидавший завершения дочерней нити, получает pthread_cancel(3C). В действительности, существуют и другие применения «отсоединенных» нитей.
Не обязательно делать pthread_detach(3C) на уже запущенную нить; в атрибутах нити (pthread_attr_t) можно указать, что нить следует запускать в уже «отсоединенном» состоянии. Это рассматривается в следующей лекции.
Разумеется, нить, над которой сделали pthread_detach(3C), не должна выделять память под возвращаемое значение при помощи malloc(3C) – ведь никто не сможет освободить эту память и это приведет к утечке памяти.
Рекомендованного стандартом способа проверить собственную «отсоединенность» нет. Из предыдущего описания можно предположить, что для проверки «отсоединенности» можно было бы использовать код возврата pthread_join(3C) для собственного идентификатора нитей – для «отсоединенных» нитей это должен быть EINVAL, а для «неотсоединенных» – EDEADLK. Для Solaris 10 и Linux 2.6 это действительно так (во всяком случае для Debian Sarge), однако в Linux 2.4 ptread_join(pthread_self(), NULL) всегда возвращает EDEADLK. Как ведет себя pthread_join на вашей системе, можно проверить при помощи программы примера 3.
Кроме нестандартности, данный способ не гарантирует, что вас не «отсоединят» после того, как вы уже осуществите проверку. То есть надежда на результат такой проверки может привести к ошибке соревнования (race condition). Поэтому применять такую проверку в реальных приложениях не следует.
Пример 3
Поскольку стандартного способа проверить «отсоединенность» нити не существует, при разработке архитектуры приложения не следует полагаться на то, что способ этой проверки вообще доступен.
Принудительное завершение нити
Библиотечная функция 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_cancel(3С)). Асинхронное прерывание требует тщательного анализа всех возможных моментов, когда оно может произойти, и обработки всех ситуаций, связанных с прерываниями в неудачные моменты. Так, если прерывание произойдет во время работы с библиотекой, которая не считается thread-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).
Стандарт 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 будет вызывать соответствующий системный вызов лишь после небольшого количества проверок.
Более сложный вопрос – зачем это было сделано. Ответ, скорее всего, состоит в том, что функции буферизованного ввода-вывода – не просто «обертки» над соответствующими системными вызовами, они осуществляют определенные (и иногда довольно сложные) операции над структурой данных, которая, собственно, и является буфером ввода-вывода. Прерывание этих библиотечных функций в неудачный момент либо приводило бы к риску оставить буфер ввода-вывода в несогласованном состоянии, либо требовало бы сложных вычислений для отслеживания и восстановления его согласованности.