Пошаговый анализ кода

Давайте теперь взглянем на основные части кода библиотеки TFP.

Начнем мы с заголовочного файла tfplib.h. Каждая программа, в которой будет использоваться библиотека идентификации, должна включать tfplib.h.

В этом файле объявлены три константы:

TFP_SIGNATURE_LENGTH
Длина сигнатуры в байтах, которую мы используем для идентификации нашего ключа.
TFP_SIGNATURE_STRING
Уникальная строка, используемая в качестве сигнатуры.
TFP_RAW_BLOCK_LENGTH
Длина двоичного блока данных в байтах.
Другой важной частью заголовочного файла является определение структуры маркера:

typedef struct tfp_signature_block_s {
char signature [TFP_SIGNATURE_LENGTH];
time_t t;
char *string;
int nstring;
unsigned char raw_block [TFP_RAW_BLOCK_LENGTH];
} tfp_signature_block_t;
Структура включает в себя следующие члены:

signature
Содержит уникальную сигнатуру. С ее помощью мы определяем что именно этот ключ в области данных потока представляет собой наш маркер.
t
Время последнего обновления маркера, выводится утилитой командной строки.
string и nstring
Маркер потока и длина маркера.
raw_block
Блок двоичных данных; используйте tfp_read_raw_block() и tfp_write_raw_block() чтобы получить к нему доступ из потока.
Поток, который хочет быть идентифицирован утилитой командной строки, вызывает функцию:

int tfp_mark (char *string);
и передает строку-маркер, которую поток будет использовать как свой идентификатор. Возвращаемое значение или EOK или errno.
Сама функция tfp_mark() определена в файле tfplib.c и выглядит вот так:

static pthread_once_t tfp_oc = PTHREAD_ONCE_INIT;
static pthread_key_t tfp_key;

static void tfp_init (void);

int
tfp_mark (char *string)
{
tfp_signature_block_t *tfpblk;

// 1) обеспечить создание ключа (только один раз)
pthread_once (&tfp;_oc, tfp_init);

// 2) проверить существование ключа
if ((tfpblk = pthread_getspecific (tfp_key)) == NULL) {

// 3) если нет, выделить под него память и заполнить ее
tfpblk = calloc (1, sizeof (*tfpblk));
if (tfpblk == NULL) {
return (errno);
}
memcpy (tfpblk, TFP_SIGNATURE_STRING, TFP_SIGNATURE_LENGTH);

// 4) связать наши данные с ключом
pthread_setspecific (tfp_key, tfpblk);
}

// 5) если строка уже существует, освободить память, которую она
// занимает, так как мы собираемся ее переписать
if (tfpblk -> string) {
free (tfpblk -> string);
}

// 6) распределить память под новую строку
tfpblk -> string = strdup (string);

// 7) запомнить длину строки
tfpblk -> nstring = strlen (tfpblk -> string) + 1;

// 8) запомнить текущее время
time (&tfpblk; -> t);

return (0);
}
Мы используем pthread_once() чтобы убедиться, что ключ для процесса создается только один раз. Работа pthread_once() заключается как раз в том, чтобы вызвать передаваемую ей функцию (tfp_init()) однократно, и в случае, когда несколько потоков одновременно пытаются получить доступ к этой функции, заблокировать их на время своего исполнения. Такое поведение идеально для использования в библиотечных вызовах и применение нами этой функции в данном случае типично. pthread_once() вызывает другую функцию — tfp_init(), которая в свою очередь вызывает pthread_key_create() (смотрите пример кода ниже, после этого объяснения).
Вызывая pthread_getspecific() мы проверяем существование ключа, связанного с нашим потоком. Если мы вызываем нашу tfp_mark() повторно для смены маркировки потока, то мы уже имеем ключ, привязанный к потоку. Если это первый вызов, то функция вернет NULL.
При первом вызове нам нужно создать пустую область данных используя calloc(). После ее создания запишем в эту область нашу сигнатуру.
После этого мы связываем созданную область данных с нашим ключом вызовом pthread_setspecific().
Этот шаг выполняется независимо от того, была ли область данных только что создана или мы она уже существовала. У нас есть две возможности оптимизации на этом шаге (мы рассмотрим их ниже, после анализа кода).
После того как мы освободили старую строку с помощью free(), мы распределяем новую используя strdup(). Заметьте, что иногда даже к законченному коду мы относимся немножко неряшливо и не проверяем возвращаемых значений. Кто-то может доказывать что это непростительно в данном случае, потому что если крошечный вызов strdup() даст сбой, ваш процесс (или системе) не поздоровиться. Да, конечно, кто-то может вызвать ее с длинной строкой (или строкой без завершающего нуля), но в данном случае она рассчитана в первую очередь на строковые константы типа «рабочий поток 1».
Мы вычисляем и запоминаем длину строки. На данный момент это выглядит глупо, как-никак строки в языке C завершаются нулем, но вы найдете этому разумное объяснение, когда мы будем рассматривать утилиту командной строки. (Я должен признаться, что поле длины было добавлено на стадии написания утилиты командной строки. Некоторые вещи гораздо проще делать если у вы владеете всем исходным кодом.)
И, наконец, просто для удобства, мы запоминаем текущее время вызывая time(). Это стоит нам четыре байта данных и ничтожное количество времени CPU по сравнению с другими операциями. Эта операция не была порождена требованиями реализации, а была добавлена интуитивно — я полагал что хранение «свежести» данных было хорошей идеей. Конечно, некоторые потоки, единожды записав свою информацию, никогда не меняли бы ее, но в некоторых случаях потоки могли бы использовать строковое поле для отчета о прогрессе работы. Именно для таких случаев и предусмотрено поле t — что бы вы могли проверить, когда было выполнено обновление данных потоком. Это же поле t обновляется каждый раз при вызове tfp_write_raw_block().