td = new throwndata();
pthread_setspecific(key, (void*)td);
// вот он - альтернативный путь доступа:
result.push(td);
}
// далее идет плодотворная работа над блоком данных *td
// . . . . . . . . .
}
int main(int argc, char **argv) {
// . . . . . .
for (int i = 0; i < N; i++)
pthread_create(NULL, NULL, GetBlock, NULL);
// . . . . . . к этому времени потоки завершились;
// ни в коем случае нельзя помещать result.size()
// непосредственно в параметр цикла!
int n = result.size();
for (int i = 0; i < n; i++) {
throwndata *d = result.front();
// обработка очередного блока *d ...
result pop();
delete d;
}
return EXIT_SUCCESS;
}
В предыдущих примерах кода мы указывали третий параметр pthread_create()
в виде &GetBlock
(адреса функции потока), но в текущем примере мы сознательно записали GetBlock
. И то и другое верно, ибо компилятор достаточно умен, чтобы при указании имени функции взять ее адрес.
Собственные данные потоков — это настолько гибкий механизм, что он может таить в себе и другие, еще не используемые техники применения.
Безопасность вызовов в потоковой среде
Рассмотрев «в первом приближении» технику собственных данных потоков, мы теперь готовы ответить на вопрос: «В чем же главное предназначение такой в общем-то достаточно громоздкой техники? И зачем для ее введения потребовалось специально расширять стандарты 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;