Такая реализация, совершенно корректная в последовательной (однопотоковой) модели, становится небезопасной в многопоточной: а) вычисление
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, вводящий в обиход собственные данные потоков).
Диспетчеризация потоков
Каждому потоку, участвующему в процессе диспетчеризации, соответствует экземпляр структуры, определенной в файле
, в котором находятся все фундаментальные для ОС QNX определения:
struct sched_param {
_INT32 sched_priority;
_INT32 sched_curpriority;
union {
_INT32 reserved[8];
struct {
_INT32 __ss_low_priority;
_INT32 __ss_max_repl;
struct timespec __ss_repl_period;
struct timespec __ss_init_budget;
} __ss;
} __ss_un;
};
#define sched_ss_low_priority __ss_un.__ss.__ss_low_priority
#define sched_ss_max_repl __ss_un.__ss.__ss_max_repl
#define sched_ss_repl_period __ss_un.__ss.__ss_repl_period
#define sched_ss_init_budget __ss_un.__ss.__ss_init_budget
Все, что определяется внутри
union __ss_un
, имеет отношение только к спорадической диспетчеризации (спорадическая диспетчеризация была введена значительно позже других, и ей будет уделено достаточно много внимания). Для всех остальных типов диспетчеризации потока это поле заполняется фиктивным полем
reserved
, и именно так в укороченном виде) определялась структура диспетчеризации в версии QNX 6.1.
Сейчас нас интересуют начальные поля этой структуры, не зависящие от типа диспетчеризации потока:
sched_priority
— статический приоритет, который присваивается потоку при его создании и который может быть программно изменен по ходу выполнения потока;