Собственные данные потоков — это настолько гибкий механизм, что он может таить в себе и другие, еще не используемые техники применения.
Безопасность вызовов в потоковой среде
Рассмотрев «в первом приближении» технику собственных данных потоков, мы теперь готовы ответить на вопрос: «В чем же главное предназначение такой в общем-то достаточно громоздкой техники? И зачем для ее введения потребовалось специально расширять стандарты POSIX?» Самое прямое ее предназначение, помимо других «попутных» применений, которые были обсуждены ранее, — это общий механизм превращения существующей функции для однопотокового исполнения в функцию, безопасную (thread safe) в многопоточном окружении. Этот механизм предлагает единую (в смысле «единообразную», а не «единственно возможную») технологию для разработчиков библиотечных модулей.
Примечание
ОС QNX, заимствующая инструментарий GNU-технологии (gcc, make, …), предусматривает возможность построения как статически связываемых библиотек (имена файлов вида
xxx.a
), так и разделяемых или динамически связываемых (имена файлов вида
xxx.so
). Целесообразность последних при построении автономных и встраиваемых систем (на что главным образом и нацелена ОС QNX) достаточно сомнительна. Однако высказанное выше положение о построении реентерабельных программных единиц относится не только к библиотечным модулям (как статическим, так и динамическим) в традиционном понимании термина «библиотека», но и охватывает куда более широкий спектр возможных объектов и в той же мере относится и просто к любым наборам утилитных объектных модулей (вида
xxx.о
), разрабатываемых в ходе реализации под целевой программный проект.
Если мы обратимся к технической документации API QNX (аналогичная картина будет и в API любого UNIX), то заметим, что только небольшая часть функций отмечена как thread safe. К «небезопасным» отнесены такие общеизвестные вызовы, как
select()
,
rand()
и
readln()
, а многим «небезопасным» в потоковой среде вызовам сопутствуют их безопасные дубликаты с суффиксом
*_r
в написании имени функции, например
MsgSend()
—
MsgSend_r()
.
В чем же состоит небезопасность в потоковой среде? В нереентерабельности функций, подготовленных для выполнения в однопоточной среде, в первую очередь связанной с потребностью в статических данных, хранящих значение от одного вызова к другому. Рассмотрим классическую функцию
rand()
, традиционно реализуемую в самых разнообразных ОС примерно так (при «удачном» выборе констант
А
,
В
,
С
):
int rand(void) {
static int x = rand_init();
return x = (A*x + B)%C;
}
Такая реализация, совершенно корректная в последовательной (однопотоковой) модели, становится небезопасной в многопоточной: а) вычисление
x
может быть прервано событием диспетчеризации, и не исключено, что вновь получивший управление поток в свою очередь обратится к
rand()
и исказит ход текущего вычисления; б) каждый поток «хотел бы» иметь свою автономную последовательность вычислений
x
, не зависящую от поведения параллельных потоков. Желаемый результат будет достигнут, если каждый поток будет иметь свой автономный экземпляр переменной
x
, что может быть получено двумя путями:
1. Изменить прототип объявления функции:
int rand_r(int *x) {
return x = (А * (*x) + В) % С;
};
При этом проблема «клонирования» переменной x в каждом из потоков (да и начальной ее инициализации) не снимается, она только переносится на плечи пользователя, что, однако, достаточно просто решается при создании потоковой функции за счет ее стека локальных переменных:
void* thrfunc(void*) {
int x = rand_init();
... = rand_r(&x);
};
Именно такова форма и многопоточного эквивалента в API QNX —
rand_r()
.
2. В этом варианте мы сохраняем прототип описания функции без изменений за счет использования различных экземпляров собственных данных потока. (Весь приведенный ниже код размещен в отдельной единице компиляции; все имена, за исключением
rand()
, невидимы и недоступны из точки вызова, что подчеркнуто явным использованием квалификатора
static
.)
static pthread_key_t key;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void destr(void* db) { delete x; }
static void once_creator(void) { pthread_key_create(&key, destr); }
int rand(void) {
pthread_once(&once, once_creator);
int *x = pthread_getspecific(key);
if (x == NULL) {
pthread_setspecific(key, x = new int);
*x = rand_init();
}
return x = (A * (*x) + B) % C;
}
В этом варианте, в отличие от предыдущего, весь код вызывающего фрагмента при переходе к многопоточной реализации остается текстуально неизменным:
void* thrfunc(void*) {
// ...
while (true) {
... = rand(x);
}
}
Перевод всего программного проекта на использование потоковой среды состоит в замене объектной единицы (объектного файла, библиотеки), содержащей реализацию
rand()
, и новой сборке приложения с этой объектной единицей.
При таком способе изменяются под потоковую безопасность и стандартные общеизвестные библиотечные функции API, написанные в своем первозданном виде 25 лет назад… (по крайней мере, так предлагает это делать стандарт POSIX, вводящий в обиход собственные данные потоков).
Диспетчеризация потоков
Каждому потоку, участвующему в процессе диспетчеризации, соответствует экземпляр структуры, определенной в файле
<sys/target_nto.h>
, в котором находятся все фундаментальные для ОС QNX определения:
struct sched_param {