Информация о нити
Во многих приложениях необходимо работать с данными, относящимися к отдельным нитям.
Например, для выполнения команды grep, если на каждый файл отводится одна нить, необходимы обработчики файлов для конкретных нитей и список найденных строк. В этих целях библиотека нитей содержит интерфейс данных для конкретных нитей.
Данные для конкретных нитей можно просмотреть в виде двумерного массива значений, причем по строкам размещены ключи, а по столбцам - ИД нитей. Ключ данных конкретной нити - это объект со скрытой реализацией типа pthread_key_t. Один и тот же ключ может применяться во всех нитях процесса. Хотя ключи для всех нитей один и те же, с этими ключами в различных нитях связаны различные данные. Благодаря этому они могут указывать на любой тип данных, например, на динамические строки или структуры.
| Ключи | Нить T1 | Нить T2 | Нить T3 | Нить T4 |
|---|---|---|---|---|
| K1 | 6 | 56 | 4 | 1 |
| K2 | 87 | 21 | 0 | 9 |
| K3 | 23 | 12 | 61 | 2 |
| K4 | 11 | 76 | 47 | 88 |
Создание и уничтожение ключей
Ключи данных для конкретных нитей необходимо создать до первого обращения. После завершения работы нити значения ее ключей автоматически уничтожаются. Кроме того, ключ можно уничтожить и по запросу на освобождение отведенной под него памяти.
Создание ключа
Для создания ключа, связанного с данными нити, нужно вызвать функцию pthread_key_create. Эта функция возвращает ключ. Значения данных, соответствующих ключу, при этом равны NULL для всех нитей, в том числе и для тех нитей, которые еще не созданы.
Пусть, например, есть две нити ( A и B). Нить A выполняет в хронологическом порядке следующие операции:
- Создает ключ данных K.
Нити A и B могут использовать ключ K. В обеих нитях этому ключу соответствует значение NULL.
- Создает нить C.
Нить C также может использовать ключ K. В нити C этому ключу соответствует значение NULL.
Максимальное число ключей для одного процесса равно 450. Это значение можно получить с помощью символьной константы PTHREAD_KEYS_MAX.
Функцию pthread_key_create можно вызвать только один раз, иначе будет создано два разных ключа. Например, рассмотрим следующий фрагмент программы:
/* глобальная переменная */
static pthread_key_t theKey;
/* нить A */
...
pthread_key_create(&theKey, NULL); /* 1-й вызов */
...
/* нить B */
...
pthread_key_create(&theKey, NULL); /* 2-й вызов */
...В этом примере нити A и B выполняются параллельно, но первый вызов происходит раньше второго. При первом вызове программа создает ключ K1 и помещает его в переменную theKey. При втором вызове программа создает другой ключ K2 и помещает его в ту же переменную theKey, поэтому ключ K1 уничтожается. В результате нить A будет работать с ключом K2, считая его ключом K1. Таких ситуаций следует избегать по следующим причинам:
- Ключ K1 потерян, поэтому отведенная под него память не будет освобождена вплоть до завершения процесса. Поскольку число ключей ограничено, их может не хватить.
- Если нить A использовала переменную theKey для хранения данных до второго вызова, эти данные были связаны с ключом K1. После второго вызова в ячейке переменной theKey хранится ключ K2; если теперь нить A попытается обратиться к своим данным, в результате она получит NULL.
Уникальность при создании ключей можно обеспечить следующими способами:
- С помощью средств однократной инициализации.
- Путем создания ключей до создания соответствующих нитей. Это можно реализовать, например, при работе с пулом нитей, данные которых предназначены для выполнения сходных операций. Этот пул обычно создается главной нитью.
Уникальность ключей должен обеспечивать программист. В библиотеке нитей нет средств, которые позволяли бы обнаружить повторное создание ключей.
Деструктор
С каждым ключом данных можно связать процедуру-деструктор. Если при завершении работы нити будут обнаружены какие-либо данные нити, не равные NULL и связанные с ключом, будет вызван деструктор для этого ключа. Он автоматически освобождает память, отведенную под данные нити, после завершения ее работы. Единственный параметр деструктора - указатель на данные нити.
pthread_key_create(&key, free);typedef struct {
FILE *stream;
char *buffer;
} data_t;
...
void destructor(void *data)
{
fclose(((data_t *)data)->stream);
free(((data_t *)data)->buffer);
free(data);
*data = NULL;
}Деструктор можно вызвать максимум четыре раза.
Удаление ключа
/* плохой пример! */
pthread_key_t key;
while (pthread_key_create(&key, NULL))
pthread_key_delete(key);Работа с данными нитей
Для доступа к данным нитей служат функции pthread_getspecific и pthread_setspecific. Функция pthread_getspecific считывает значение, связанное с указанным в параметре ключом и относящееся к вызывающей нити; функция pthread_setspecific устанавливает это значение.
Изменение значений
private_data = malloc(...);
pthread_setspecific(key, private_data);
pthread_setspecific(key, old);
...
pthread_setspecific(key, new);int swap_specific(pthread_key_t key, void **old_pt, void *new)
{
*old_pt = pthread_getspecific(key);
if (*old_pt == NULL)
return -1;
else
return pthread_setspecific(key, new);
}Такой процедуры нет в библиотеке нитей, так как возвращать предыдущее значение данных нити не всегда нужно. Такая необходимость возникает, например, если данные нити являются указателями на специальные области пула памяти, выделенные главной нитью.
Работа с деструкторами
pthread_key_create(&key, free);
...
...
private_data = malloc(...);
pthread_setspecific(key, private_data);
...
/* плохой пример! */
...
pthread_getspecific(key, &data);
free(data);
...При завершении работы нити вызывается деструктор, который освобождает память, отведенную под данные нити. Поскольку значение данных - указатель на уже освобожденную память, то может возникнуть ошибка. В следующем примере ошибка исправлена:
/* правильный код */
...
pthread_getspecific(key, &data);
free(data);
pthread_setspecific(key, NULL);
...При завершении работы нити деструктор не вызывается, поскольку все данные уже освобождены.
Работа с другими типами данных
Структура данных позволяет хранить значения, не являющиеся указателями, однако это не рекомендуется по следующим причинам:
- Преобразование указателей в скалярные типы может нарушить переносимость программ
- Значение указателя NULL зависит от реализации; в некоторых системах указателю NULL соответствует ненулевое значение.
Если вы уверены, что ваша программа никогда не будет переноситься в другую систему, то можете работать с целочисленными данными.