// STL-очередь, например указателей на экземпляры данных
queue<throwndata*> result;
// функция потока
void* GetBlock(void*) {
pthread_once(&once, createkey);
throwndata *td;
if ((td = (throwndata*)pthread_getspecific(key)) == NULL) {
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 в каждом из потоков (да и начальной ее инициализации) не снимается, она только переносится на плечи пользователя, что, однако, достаточно просто решается при создании потоковой функции за счет ее стека локальных переменных: