Атрибуты нитей и управление нитями
Так встань у реки, смотри как течет река;
Ее не поймать ни в сеть, ни рукой.
Она безымянна, ведь имя есть лишь у ее берегов;
Прими свое имя и стань рекой.
Атрибуты нитей
При создании нити можно указать блок атрибутов нити при помощи второго параметра функции pthread_create(3C). Этот блок представляет собой структуру pthread_attr_t.
Стандарт POSIX требует рассматривать эту структуру как непрозрачный тип и использовать для изменения отдельных атрибутов функции просмотра и установки отдельных атрибутов. Для инициализации pthread_attr_t следует использовать функцию pthread_attr_init(3С). Эта функция имеет единственный параметр – pthread_attr_t *attr. Она устанавливает атрибуты в соответствии с табл. 4.1.
Атрибут | Значение по умолчанию | Объяснение |
---|---|---|
scope | PTHREAD_SCOPE_PROCESS | Нить использует процессорное время, выделяемое процессу. В Solaris 9 и последующих версиях Solaris этот параметр не имеет практического значения |
detachstate | PTHREAD_CREATE_JOINABLE | Нити создаются присоединенными (для освобождения ресурсов после завершения нити необходимо вызвать pthread_join(3C) ). |
stackaddr | NULL | Стек нити размещается системой |
stacksize | 0 | Стек нити имеет размер, определяемый системой |
priority | 0 | Нить имеет приоритет 0 |
inheritsched | PTHREAD_EXPLICIT_SCHED | Нить не наследует приоритет у родителя |
schedpolicy | SCHED_OTHER | Нить использует фиксированные приоритеты, задаваемые ОС. Используется вытесняющая многозадачность (нить исполняется, пока не будет вытеснена другой нитью или не заблокируется на примитиве синхронизации) |
Кроме этих атрибутов, pthread_attr_t содержит некоторые другие атрибуты. Некоторые из этих атрибутов игнорируются Solaris 10, и введены для совместимости с другими реализациями POSIX Thread API.
Для изменения значений атрибутов необходимо использовать специальные функции. Для каждого атрибута определены две функции, pthread_attr_set%ATTRNAME% и pthread_attr_get%ATTRNAME%.
При создании нити используются те значения атрибутов, которые были заданы к моменту вызова pthread_create(3C). Изменения в pthread_attr_t, внесенные уже после запуска нити, игнорируются системой. Благодаря этому один и тот же блок атрибутов нити можно использовать для создания нескольких разных нитей с разными атрибутами.
Некоторые из атрибутов, например detachstate и priority, могут быть изменены у уже работающей нити. Для этого используются отдельные функции. Так, detachstate в атрибутах нити изменяется функцией pthread_attr_setdetachstate(3C), а у исполняющейся нити – функцией pthread_detach(3C). Функция pthread_detach(3C) обсуждалась на предыдущей лекции. pthread_detach (3С) изменяет значение атрибута с JOINABLE на DETACHED ; способа изменить этот атрибут с DETACHED на JOINABLE у работающей нити не существует.
Многие из атрибутов, например размер или местоположение стека, не могут быть изменены у работающей нити.
Перед уничтожением структуры pthread_attr_t необходимо вызвать функцию pthread_attr_destroy(3C).
Ниже приводится список атрибутов нити с их допустимыми значениями и кратким описанием семантики.
Scope (область действия). Допустимые значения: PTHREAD_SCOPE_SYSTEM и PTHREAD_SCOPE_PROCESS. Значение PTHREAD_SCOPE_SYSTEM означает, что нить планируется системным планировщиком и соревнуется за системные ресурсы с другими процессами. Значение PTHREAD_SCOPE_PROCESS означает, что нить планируется пользовательским планировщиком и, с точки зрения системы, считается частью своего процесса. В системах с гибридным планировщиком нити с областью действия SYSTEM соответствует так называемым "привязанным" (bound) нитям, которые привязаны к определенному LWP. В Solaris 9 и 10 все нити исполняются в собственных LWP, поэтому установка атрибута scope не имеет практического значения (возможно, правильнее было бы указать PTHREAD_SCOPE_SYSTEM как значение по умолчанию).
Detachstate (присоединение/отсоединение). Допустимые значения: PTHREAD_CREATE_JOINABLE и PTHREAD_CREATE_DETACHED. У нитей в состоянии DETACHED ресурсы (стек, Thread Local Data) освобождаются сразу после завершения нити.
У нитей в состоянии JOINABLE ресурсы освобождаются после того, как другая нить вызовет pthread_join(3C).
Stacksize (размер стека). Допустимые значения – 0 или размер стека в байтах. 0 означает, что система сама определяет размер стека. В Solaris на 32-битных платформах под стек выделяется 1 мегабайт памяти, на 64-битных – 2 мегабайта. Запрещено выделять под стек меньше памяти, чем PTHREAD_STACK_MIN. Эта константа определена в <limits.h> и в <pthread.h>. Попытка указать стек меньшего размера может привести к аварийному завершению вашей программы, скорее всего(но не обязательно) по SIGSEGV.
Эксперименты показывают, что простые программы могут успешно исполняться со стеками, значительно меньшими, чем PTHREAD_STACK_MIN, но, разумеется, никто не гарантирует, что они смогут исполняться таким образом при всех возможных сочетаниях обстоятельств.
При разработке реальных программ необходимо иметь в виду, что значение PTHREAD_STACK_MIN учитывает только потребности POSIX Thread Library, но не потребности вашего кода и не потребности библиотек, которые ваш код может вызывать. В рамках нашего курса мы не изучаем средств, при помощи которых можно определить реальные потребности вашей программы в памяти под стек.
Stackaddr (адрес стека). Допустимые значения – NULL или указатель на область памяти, достаточную для размещения стека нити. NULL означает, что система должна разместить область под стек при создании нити. При выделении области под стек необходимо проявлять осторожность. Наиболее важный из практических аспектов – что если вы выделяли память под стек сами, то даже у отсоединенных нитей система не будет освобождать эту память (потому что система, вообще говоря, не знает, каким способом вы выделяли эту память). Разумеется, освобождать память из-под стека можно только после успешного завершения pthread_join(3C). Освобождение памяти из-под стека отсоединенных нитей представляет собой трудноразрешимую на практике проблему.
Как правило, не рекомендуется размещать память под стек самостоятельно. В большинстве случаев разумнее доверить эту работу системе.
Concurrency (степень параллелизма). Допустимые значения – положительные целые числа.
В системах с гибридным планировщиком значение атрибута с определенными допущениями соответствует количеству LWP, создаваемых системой для вашего процесса.
В Solaris 9 и 10 установка concurrency игнорируется системой. Сам атрибут сохраняется для совместимости со стандартом POSIX и старыми приложениями.
Schedpolicy (политика планирования). Допустимые значения – SCHED_FIFO, SCHED_RR и SCHED_OTHER. Политики SCHED_FIFO и SCHED_RR используются процессами реального времени.
FIFO (First-In First-Out, первый вошел, первый вышел) соответствует планированию в порядке линейной очереди; нити получают управление и отдают его только когда остановятся на примитиве синхронизации или вызовут sched_yield(3RT).
RR (Round-Robin, кольцевая очередь) соответствует планированию с квантами времени, когда нити исполняются либо до момента явной отдачи управления, либо до истечения кванта времени. При истечении кванта времени нить устанавливается в конец очереди. В Solaris процессы и нити реального времени может исполнять только суперпользователь с uid==0. Установка политик SCHED_FIFO и SCHED_RR другими пользователями не является ошибкой, однако нить по прежнему исполняется с классом планирования, унаследованным от родительского процесса.
SCHED_OTHER соответствует системной политике планирования. В Solaris нити наследуют политику планирования и приоритет своего процесса. При помощи системного вызова priocntl(2) можно назначать приоритет и класс планирования для как для отдельных LWP, так и для процессов в целом и для групп процессов. Этот способ управления политиками планирования обеспечивает гораздо большую гибкость, чем POSIX Thread API, но он нестандартен. Так, в Linux для управления приоритетами используются вызовы get/setpriority(2), а понятия класса планирования в Linux вообще нет. Наиболее портабельный API для управления приоритетами в системах семейства Unix – это системный вызов nice(2), но этот API отличается негибкостью и также не позволяет управлять классами планирования.
Все системы Unix SVR4 поддерживают два класса планирования – TS (Time Share, разделяемое время) и RT (Real Time, реальное время).
Time Share – система оптимизирует среднее время реакции на внешние события. Используется простая динамическая приоритизация, при которой приоритет процесса складывается из двух компонентов: базового приоритета (nice level) и штрафа за исчерпание кванта времени. Базовый приоритет задается системным вызовом nice(2), чем больше значение nice, тем ниже приоритет. Штраф накладывается на нити, которые были сняты системой по истечению кванта времени, и увеличивается с каждым последующим таким снятием. Если нить исполняет блокирующийся системный вызов, штраф снимается. На практике это приводит к тому, что у процессов и LWP, исполняющих блокирующиеся системные вызовы (такие процессы вносят наибольший вклад во время реакции системы), относительный приоритет повышается.
RealTime – используется статическая приоритизация. LWP реального времени исполняется до тех пор, пока не будет вытеснен LWP с более высоким приоритетом (это может произойти либо в результате выхода другого LWP из блокирующегося системного вызова, либо в результате изменения приоритета нашего LWP), либо пока сам не отдаст управление, исполнив блокирующиеся системный вызов или sched_yield(3RT). LWP реального времени имеют более высокий приоритет, чем любые процессы разделенного времени, а также более высокий приоритет, чем большинство нитей ядра. Только суперпользователь имеет право запускать LWP и процессы реального времени. Solaris также поддерживает дополнительные классы планирования – FX (Fixed Priority, фиксированные приоритеты) и FSS (Fair Share Scheduling, справедливое планирование). Fair Share Scheduling – сложная схема динамической приоритизации, обеспечивающая определенным группам процессов гарантированную долю процессорного времени.
Используется при планировании заданий в рамках проектов и контейнеров Solaris. Модули ядра, реализующие классы планирования FSS и FX, входят в стандартную поставку Solaris 10, однако администратор системы может не загружать эти модули, тогда соответствующие классы планирования будут недоступны. Документация по настройке системы настоятельно не рекомендует устанавливать классы планирования TS и FSS на одном процессоре, потому что процессы с этими классами планирования используют одни и те же уровни приоритетов; соревнование таких процессов за один процессор может приводить к труднопредсказуемым и, как правило, нежелательным явлениям. Как правило, рекомендуется устанавливать классы планирования FSS и TS на разных процессорах многопроцессорных компьютеров, или использовать только один из этих классов планирования.
Inheritsched (наследование класса планирования). Допустимые значения – PTHREAD_INHERIT_SCHED и PTHREAD_EXPLICIT_SCHED. INHERIT (наследовать) означает, что нить наследует класс планирования и приоритет от родительской нити, EXPLICIT (задавать явно) означает, что класс планирования и приоритет вновь создаваемой нити должны быть указаны явно в атрибуте schedparam
Schedparam (параметры планирования). Допустимое значения – struct sched_param, определенная в <sched.h>. В соответствии со стандартом POSIX эта структура должна содержать поле sched_priority, соответствующее приоритету нити. В сочетании с политикой планирования SCHED_OTHER в Solaris 10 используются также поля sched_nice и sched_nicelim. Guardsize (размер сторожевой области). Допустимые значения – 0 или желаемый размер сторожевой области в байтах. Значение 0 указывает, что сторожевую область выделять не надо.
Сторожевая область (guard area) – это страницы или сегменты, защищенные от чтения и записи, размещаемые в начале области, выделяемой под стек. Если стек нити при своем росте достигает сторожевой области, программа завершается по SIGSEGV. Это используется для защиты от переполнения стека, которое может происходить из-за бесконечной рекурсии или злоупотребления большими структурами данных, размещаемыми в стеке. Некоторые языки программирования допускают размещение в стеке динамических структур данных. В C99 допускаются динамические массивы, во многих реализациях языка C память в стеке можно размещать при помощи нестандартной функции alloca(3C).
Неконтролируемое переполнение стека приводит к неконтролируемому разрушению размещенных в памяти структур данных и непредсказуемым последствиям. Использование сторожевой области, во всяком случае, гарантирует, что SIGSEGV произойдет вскоре после собственно переполнения. Это обычно сильно упрощает отладку программы.
Размер сторожевой области следует выбирать исходя из размера структур данных, которые ваша программа размещает в стеке. Система имеет право округлять этот размер вверх до числа, кратного размеру страницы диспетчера памяти (этот размер можно получить системным вызовом sysconf(2) с параметром PAGESIZE ). Однако pthread_attr_getguardsize(3C) возвращает значение guardsize без учета округления.
По умолчанию, система выделяет под сторожевую область одну страницу.
Дополнительные функции по управлению нитями
Для инициализации какого-либо динамического пакета в многопоточной программе рекомендуется использовать функцию pthread_once(3C). Параметры этой функции:
- pthread_once_t *flag – флаговая переменная, которая контролирует, вызывалась функция или еще нет
- void (*init_routine)(void) – функция, которую следует вызвать.
Pthread_once(3C) гарантирует, что init_routine будет вызвана один раз. Если вы вызовете pthread_once с одной и той же флаговой переменной из двух разных нитей, то функция init_routine будет вызвана в одной нити и не будет вызвана в другой. Перед использованием pthread_once необходимо инициализировать флаговую переменную константой PTHREAD_ONCE_INIT. Результаты вызова pthread_once с неинициализированной флаговой переменной или с флаговой переменной с классом памяти auto непредсказуемы.
Сама по себе функция pthread_once(3C) не является точкой прерывания. Однако если init_routine содержит точки прерывания и нить будет прервана во время исполнения этой функции, результат будет такой, как будто init_routine не вызывалась. Т.е. при вызове pthread_once(3С) из другой нити init_routine будет вызвана повторно. Для передачи управления между нитями можно использовать нестандартную функцию sched_yield(3RT). Эта функция снимает текущую нить с процессора, но не позволяет контролировать, какой из других нитей этого или другого процесса будет передан процессор. Эта функция включена в курс потому, что без нее может быть сложно решить задачу 10. Однако этой функцией не следует злоупотреблять.
Если функциональность вашей программы зависит от расстановки в коде вызовов sched_yield(3RT), то это ошибочная программа. Результат исполнения такой программы будет также зависеть от количества процессоров в системе, от распределения нитей вашей программы между процессорами, а также от наличия в системе других активных нитей и процессов и от того, чем именно занимаются эти процессы.
Таким образом, sched_yield(3RT) можно использовать лишь для выполнения нефункциональных требований, скорее всего связанных с временем реакции на события. По видимому, именно из этих соображений в Solaris 10 эта функция включена в библиотеку librt.so (так называемую библиотеку реального времени), а не в основной API. При сборке программ, использующих sched_yield(3RT) необходимо подключать эту библиотеку ключом –lrt.
Функция sched_yield(3RT) нестандартна; в Linux аналогичная функция называется pthread_yield(3PTHREAD), в других реализациях POSIX Thread API аналогичных функций может вообще не быть.