Атрибуты нитей и управление нитями
Приватные данные нити
Приватные или локальные данные нити (Thread Specific Data) – это переменные, создаваемые функцией pthread_key_create(3C). Эти переменные имеют тип void * и идентифицируются ключами pthread_key_t. Для каждой нити, которая пытается обратиться к переменной, идентифицируемой определенным ключом, создается собственная копия значения соответствующей переменной.
Параметры функции pthread_key_create(3C):
- pthread_key_t *key – ключ, идентифицирующий переменную локальных данных. Это непрозрачный тип, внутренняя структура которого зависит от реализации POSIXThread API.
- void (*destructor)(void *) – необязательная функция-деструктор.
При завершении нити, если значение деструктора отлично от нуля и если значение самой переменной отлично от нуля, будет вызван деструктор, а в качестве параметра ему будет передано значение переменной. Например, если в качестве значения переменной используется блок памяти, выделенный при помощи malloc(3C), в деструкторе следует освободить эту память при помощи free(3C). Деструктор должен продемонстрировать свое завершение, установив значение переменной в NULL. Деструкторы вызываются в любом порядке, но если после отработки всех деструкторов значения некоторых переменных не равны нулю, деструкторы этих переменных будут вызваны повторно. Количество повторных вызовов деструкторов определяется константой PTHREAD_DESTRUCTOR_ITERATIONS.
Многократный вызов pthread_key_create(3C) с одной и той же переменной-ключом, вообще говоря, приводит к многократному созданию ключей, поэтому pthread_key_create(3С) следует вызывать либо до создания нитей, работающих с этим ключом, либо при помощи pthread_once(3C).
Доступ к значению локальной переменной осуществляется функциями pthread_setspecific(3C) и pthread_getspecific(3C). Если вызвать get до первого вызова set из той же нити, будет получено значение NULL.
Приватные данные нитей широко используются при переделке небезопасных при многопоточном исполнении программ в безопасные. Действительно, основной источник проблем с многопоточностью – это статические переменные, используемые внутри библиотеки. Преобразовав обращения к таким переменным в thread-specific data, мы в большинстве случаем можем получить корректно работающую версию библиотеки. Например, функция strtok(3C) при первом вызове разбивает строку-параметр на токены в соответствии с заданными разделителями и возвращает первый токен. При последующих вызовах она возвращает второй, третий и т.д. токены. Для этого она должна где-то хранить указатель на текущий токен; однопоточные версии стандартной библиотеки языка C обычно используют для этого статическую переменную. Разумеется, если в другой нити эта функция будет вызвана с другой начальной строкой, этот указатель будет перезаписан. Результат следующего вызова strtok(3C) в первой нити при этом будет сильно отличаться от того, что, скорее всего, ожидал программист, разрабатывавший программу этой нити.
Если заменить статическую переменную на TSD – как это сделано в libc, входящей в поставку Solaris 10 – мы получим многопоточную версию strtok(3C), которая ведет себя в соответствии с разумными ожиданиями. При этом большинство библиотек, использующих strtok(3C) разумным образом, также станут безопасны с точки зрения многопоточности (разумеется, при условии, что в них нет других проблем с многопоточностью).
Очевидно, что такой подход не является универсальным. Так, если подвергнуть такому преобразованию реализацию malloc(3C)/free(3C), это приведет к тому, что у каждой нити будет свой пул памяти, и память, выделенную в одной нити, нельзя будет освобождать в другой нити. Такое поведение нельзя назвать соответствующим разумным ожиданиям.
Мониторинг и отладка
Использование ядерных нитей дает определенные преимущества при анализе поведения процесса. Поскольку все нити процесса соответствуют системным объектам, стандартные средства мониторинга системы могут показать нам, сколько у процесса нитей и чем они занимаются в данный момент.
В Linux нити процесса имеют собственный pid(ProcessIdentifier) и показываются как отдельные записи в таблице процессов, например в выводе команд ps(1) и top(1).
В Solaris, по умолчанию процесс вместе со всеми своими нитями показывается как одна строка в выводе команд ps(1) и prstat(1). У команды ps(1) некоторые опции, в частности параметр nlwp у опции –o, позволяют вывести количество LWP у процесса. У команды prstat(1) количество LWP выводится в последней колонке, через / после имени программы (см. пример 4.1).
PID USERNAME SIZE RSS STATE PRI NICE TIME CPU PROCESS/NLWP 10294 vinokuro 123M 51M sleep 59 0 0:02:05 0.0% java/23 11430 fat 4720K 4192K cpu0 59 0 0:00:00 0.0% prstat/1 744 root 1924K 1264K sleep 59 0 0:00:00 0.0% snmpdx/1 646 root 3440K 1828K sleep 59 0 0:00:00 0.0% syslogd/13 713 root 1680K 796K sleep 59 0 0:00:00 0.0% smcboot/1 714 root 1680K 796K sleep 59 0 0:00:00 0.0% smcboot/1 641 daemon 4216K 3700K sleep 59 0 0:00:01 0.0% nfsmapid/4 635 root 1680K 1032K sleep 59 0 0:00:00 0.0% sac/1 637 root 1788K 1212K sleep 59 0 0:00:00 0.0% ttymon/1 634 root 1048K 720K sleep 59 0 0:00:00 0.0% utmpd/1 639 root 3584K 2576K sleep 59 0 0:00:00 0.0% inetd/3 619 daemon 2476K 1836K sleep 59 0 0:00:00 0.0% statd/1 627 daemon 2040K 1452K sleep 59 0 0:00:00 0.0% lockd/2 578 root 5248K 4068K sleep 59 0 0:00:43 0.0% nscd/33 524 root 6172K 5340K sleep 59 0 0:00:08 0.0% svc.configd/12 522 root 8684K 6448K sleep 59 0 0:00:30 0.0% svc.startd/12 554 daemon 3908K 2332K sleep 59 0 0:00:01 0.0% kcfd/4 625 root 2016K 1500K sleep 59 0 0:00:00 0.0% ypbind/1 520 root 1992K 1228K sleep 59 0 0:00:00 0.0% init/1 507 root 0K 0K sleep 60 -0:00:00 0.0% zsched/1 5719 belenky 944K 600K sleep 59 0 0:00:00 0.0% a.out/1 Total: 66 processes, 164 lwps, load averages: 0.00, 0.00, 0.004.1. вывод команды prstat
Команда ps –L выводит информацию по каждому LWP отдельной строкой. Ключ –L совместим с большинством других ключей, например можно написать ps –Lfu $USER. Аналогично ведет себя команда prstat –L.
Информация обо всех LWP процесса доступна в каталоге этого процесса в псевдофайловой системе /proc. Эта информация доступна в подкаталоге /proc/$PID/lwp/$LWPID. Структуры размещенных в этом подкаталоге файлов описаны в proc(4).
Ряд утилит, работающих с proc(4) позволяют получать информацию об LWP, например утилита pstack(1) выдает дамп стеков всех LWP указанного процесса (см. пример 4.2). Pstack(1) приостанавливает исполнение процесса на время выдачи стека, но в целом не причиняет ему никакого вреда. Исследуя выдачу pstack(1) часто удается узнать много нового про свою программу и много забавного про реализацию библиотечных функций.
new AB new A new C new BOLT new A ^Z [1]+ Stopped ./production_line -bash-3.00$ ps PID TTY TIME CMD 11536 pts/9 0:00 ps 11410 pts/9 0:00 bash 11535 pts/9 0:00 producti -bash-3.00$ pstack 11535 11535: ./production_line -----------------lwp# 1 / thread# 1 ------------------- fef7d2a9 lwp_park (0, 0, 0) fef733a0 sema_wait (8060dbc) + d fefb565b sem_wait (8060dbc) + 27 08050a89 createWidget (8047d60, 8047cc4, feffa824, 5, 4, 3) + 19 08050b6c main (1, 8047d08, 8047d10) + ac 0805088a _start (1, 8047dc8, 0, 8047dda, 8047de5, 8047df5) + 7a -----------------lwp# 2 / thread# 2 -------------------fef7d5e5 nanosleep (fee59fb0, fee59fb8) 08050936 createA (0) + 16 fef7cf3f _thr_setup (fee82400) + 4e fef7d230 _lwp_start (fee82400, 0, 0, fee59ff8, fef7d230, fee82400) -----------------lwp# 3 / thread# 3 -------------------fef7d5e5 nanosleep (fed4dfb0, fed4dfb8) 08050986 createB (0) + 16 fef7cf3f _thr_setup (fec30000) + 4e fef7d230 _lwp_start (fec30000, 0, 0, fed4dff8, fef7d230, fec30000) -----------------lwp# 4 / thread# 4 -------------------fef7d5e5 nanosleep (fec2dfb0, fec2dfb8) 080509d6 createC (0) + 16 fef7cf3f _thr_setup (fec30400) + 4e fef7d230 _lwp_start (fec30400, 0, 0, fec2dff8, fef7d230, fec30400) -----------------lwp# 5 / thread# 5 -------------------fef7d2a9 lwp_park (0, 0, 0) fef733a0 sema_wait (8060d5c) + d fefb565b sem_wait (8060d5c) + 27 08050a36 createAB (0) + 26 fef7cf3f _thr_setup (fec30800) + 4e fef7d230 _lwp_start (fec30800, 0, 0, feb2dff8, fef7d230, fec30800) -bash-3.00$4.2. Выдача программы pstack(1)
Однако наиболее мощным средством анализа поведения программ считаются отладчики.
Для использования отладчиков программу рекомендуется компилировать и собирать с ключом –g (генерировать отладочную информацию). Это позволяет вести отладку в терминах строк исходного кода и переменных. Без отладочной информации доступна только отладка деассемблированного кода. Это в равной мере относится ко всем отладчикам, рассматриваемым далее на этой лекции.
Наиболее распространенный отладчик для систем семейства Unix, gdb (GNU Debugger) поддерживает многопоточную отладку на большинстве современных ОС, однако при сборке отладчика эту возможность можно выключить.
Проверить поддержку многопоточности в вашей версии gdb можно при помощи команды info threads (см. пример 1.1 в "Введение" ).