Информация о нити

Во многих приложениях необходимо работать с данными, относящимися к отдельным нитям.

Например, для выполнения команды grep, если на каждый файл отводится одна нить, необходимы обработчики файлов для конкретных нитей и список найденных строк. В этих целях библиотека нитей содержит интерфейс данных для конкретных нитей.

Данные для конкретных нитей можно просмотреть в виде двумерного массива значений, причем по строкам размещены ключи, а по столбцам - ИД нитей. Ключ данных конкретной нити - это объект со скрытой реализацией типа pthread_key_t. Один и тот же ключ может применяться во всех нитях процесса. Хотя ключи для всех нитей один и те же, с этими ключами в различных нитях связаны различные данные. Благодаря этому они могут указывать на любой тип данных, например, на динамические строки или структуры.

На следующем рисунке в нити T2 значение данных 12 связано с ключом K3. В нити T4 с тем же ключом связано значение 2.
Ключи Нить 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 выполняет в хронологическом порядке следующие операции:

  1. Создает ключ данных K.

    Нити A и B могут использовать ключ K. В обеих нитях этому ключу соответствует значение NULL.

  2. Создает нить 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 и связанные с ключом, будет вызван деструктор для этого ключа. Он автоматически освобождает память, отведенную под данные нити, после завершения ее работы. Единственный параметр деструктора - указатель на данные нити.

Ключи данных можно использовать, например, для динамически выделенных буферов. Для освобождения буферов после завершения работы нити следует вызвать деструктор; можно применять функцию free:

pthread_key_create(&key, free);
Деструкторы могут иметь и более сложную структуру. Если команда grep с одной нитью на каждый файл хранит в области данных нитей структуру, в которую входит рабочий буфер и дескриптор файла нити, то деструктор может выглядеть следующим образом:
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_delete. Функция pthread_key_delete не вызывает деструктор для каждой нити с данными. После уничтожения ключ данных можно повторно создать функцией pthread_key_create. Следовательно, функция pthread_key_delete оказывается полезной и при работе с несколькими ключами данных. Например, в приведенном фрагменте программы содержится бесконечный цикл:

/* плохой пример! */
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);
При установке нового значения предыдущее значение теряется. Например, в приведенном ниже фрагменте программы значение указателя old теряется, а память, на которую ссылается этот указатель, превращается в "мусор":

pthread_setspecific(key, old);
...
pthread_setspecific(key, new);
Ответственность за сохранение старых указателей, т.е. за образование "мусора", несет программист. Например, можно реализовать процедуру swap_specific следующим образом:
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 вызов деструктора. Программист должен также присвоить указателю на освобожденную память значение NULL. В противном случае возможен вызов деструктора с недопустимыми параметрами. Например:

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 соответствует ненулевое значение.

Если вы уверены, что ваша программа никогда не будет переноситься в другую систему, то можете работать с целочисленными данными.