Положим, что нам требуется создать
N
параллельно исполняющихся идентичных потоков (использующих единую функцию потока), каждый из которых предполагает работать со своей копией экземпляра данных типа
DataBlock
:
class DataBlock {
~DataBlock() { ... }
...
};
void* ThreadProc(void *data) {
// ... здесь будет код, который мы рассмотрим
return NULL;
}
...
for (int i = 0; i < N; i++)
pthread_create(NULL, NULL, &ThreadProc, NULL);
Последовательность действий потока выглядит следующим образом:
1. Поток запрашивает
pthread_key_create()
— создание ключа для доступа к блоку данных
DataBlock
. Если потоку необходимо иметь несколько (m) блоков собственных данных различной типизации (и различного функционального назначения):
DataBlock_1
,
DataBlock_2
…
DataBlock_m
, то он запрашивает значения ключей соответствующее число раз для каждого типа (
m
).
2. Неприятность здесь состоит в том, что запросить значение ключа для
DataBlock
должен только первый пришедший к этому месту поток (когда ключ еще не распределен). Последующие потоки, достигшие этого места, должны только воспользоваться ранее распределенным значением ключа для типа
DataBlock
. Для разрешения этой сложности в систему функций собственных данных введена функция
pthread_once()
.
3. После этого каждый поток (как создавший ключ, так и использующий его) должен запросить по
pthread_getspecific()
адрес блока данных и, убедившись, что это
NULL
, динамически распределить область памяти для своего экземпляра данных, а также зафиксировать по
pthread_setspecific()
этот адрес в массиве экземпляров для дальнейшего использования.
4. Дальше поток может работать с собственным экземпляром данных (отдельный экземпляр на каждый поток), используя для доступа к нему
pthread_getspecific()
.
5. При завершении любого потока система уничтожит и его экземпляр данных, вызвав для него деструктор, который был установлен вызовом
pthread_key_create()
, единым для всех экземпляров данных, ассоциированных с этим значением ключа.
Теперь запишем это в коде, заодно трансформировав в новую функцию
ThreadProc()
код ранее созданной версии этой же функции
SingleProc()
для исполнения в одном потоке, не являющийся реентерабельным и безопасным в многопоточной среде. (О вопросах реентерабельности мы обязательно поговорим позже.)
void* SingleProc(void *data) {
static DataBlock db( ... );
// ... операции с полями DataBlock
return NULL;
}
To, что типы параметров и возвращаемое значение
SingleProc()
«подогнаны» под синтаксис ее более позднего эквивалента
ThreadProc()
, не является принципиальным ограничением - входную и выходную трансформации форматов данных реально осуществляют именно в многопоточном эквиваленте. Нам здесь важно принципиально рассмотреть общую формальную технику трансформации нереентерабельного кода в реентерабельный.
Далее следует код
SingleProc()
, преобразованный в многопоточный вид:
static pthread_key_t key;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void destructor(void* db) {
delete (DataBlock*)db;
}
static void once_creator(void) {
// создается единый на процесс ключ для данных DataBlock:
pthread_key_create(&key, destructor);
}
void* ThreadProc(void *data) {
// гарантия того, что ключ инициализируется только 1 раз на процесс!
pthread_once(&once, once_creator);
if (pthread_getspecific(key) == NULL)
pthread_setspecific(key, new DataBlock(...));
// Теперь каждый раз в теле этой функции или функций, вызываемых
// из нее, мы всегда можем получить доступ к экземпляру данных