Скачиваний:
90
Добавлен:
12.05.2015
Размер:
913.92 Кб
Скачать

14.7. Реентерабельность

В разделе 10.6 обсуждались обработчики сигналов и реентерабельные функции. Потоки в чем-то похожи на обработчики сигналов, когда дело касается реентерабельности. Как и в случае с обработчиками сигналов, в многопоточных приложениях вполне возможно возникновение ситуации, когда одну и ту же функцию одновременно вызывают несколько потоков.

Функции, которые могут безопасно вызываться одновременно из нескольких потоков, называются безопасными в многопоточной среде (thread-safe). Многие из функций не являются безопасными, потому что они возвращают результаты в буфере, размещенном статически. Эти функции можно сделать безопасными, изменив интерфейс их вызова. Для этого нужно, чтобы вызывающая программа предоставила свой буфер для записи результата. Реализации, которые поддерживают функции, безопасные в многопоточной среде, определяют в заголовочном файле <unistd.h> символ _POSIX_THREAD_SAFE_FUNCTIONS. Кроме того, для проверки поддержки безопасных функций во время выполнения приложения могут вызывать функцию sysconf с аргументом _SC_THREAD_SAFE_FUNCTIONS. Все реализации, отвечающие требованиям стандарта SUS, обязаны обеспечить поддержку безопасных функций. Некоторые безопасные функции, имеющиеся в ОС Linux, перечислены в табл. 14.2. Суффикс _r в конце имен функций является признаком их реентерабельности.

Таблица 14.2

Альтернативные версии функций, безопасные в многопоточной среде

asctime_r

ctime_r

getgrgid_r

getgrnam_r

getlogin_r

getpwnam_r

getpwuid_r

gmtime_r

localtime_r

rand_r

readdir_r

strerror_r

strtok_r

ttyname_r

Если функция является реентерабельной по отношению к потокам, то такая функция называется безопасной в многопоточной среде. Однако это не говорит о том, что функция реентерабельна по отношению к обработчикам сигналов. Если функция может безопасно вызываться из обработчиков асинхронных сигналов, то такая функция называется безопасной в контексте обработки асинхронных сигналов. Функции, безопасные по отношению к обработчикам сигналов, перечислялись в табл. 10.3 при обсуждении реентерабельных функций (раздел 10.6).

14.8. Локальные данные потоков

Локальные данные потока – это механизм хранения и поиска данных, связанных только с конкретным потоком. Локальные данные потока нужны для того, чтобы каждый поток мог обладать некоторым набором данных, принадлежащих ему одному, и не беспокоиться по поводу синхронизации при работе с этими данными.

Люди приложили огромные усилия для разработки модели совместного использования ресурсов и атрибутов в многопоточных приложениях. Итак, зачем же нам нужны интерфейсы, которые препятствуют использованию этой модели? На то существуют две причины.

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

Вторая причина заключается в том, что механизм организации локальных данных потока предоставляет возможность адаптации интерфейсов процессов к многопоточной среде. Типичный пример такой адаптации – переменная errno (раздел 1.9). Старые интерфейсы (которые были определены еще до появления концепции потоков) рассматривают errno как целочисленную переменную с глобальной областью видимости в пределах процесса. Системные вызовы и библиотечные функции в случае неудачи записывают в эту переменную код ошибки. Чтобы позволить потокам использовать те же самые системные вызовы и библиотечные функции, переменная errno была переопределена как локальная переменная потока. Таким образом, теперь, когда поток вызывает функцию, которая изменяет значение переменной errno, он уже не оказывает действия на другие потоки в рамках процесса.

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

Перед размещением локальных данных потока мы должны создать ключ, с помощью которого будут идентифицироваться данные. Этот ключ будет использоваться для получения доступа к локальным данным потока. Создается такой ключ с помощью вызова функции pthread_key_create.

#include <pthread.h>

int pthread_key_create (pthread_key_t *keyp, void (*destructor)(void *));

/* функция возвращает 0 в случае успеха, код ошибки – в случае неудачи */

Созданный ключ сохраняется по адресу keyp. Один и тот же ключ может использоваться различными потоками в рамках процесса, но каждый поток будет связывать с ключом отдельный набор локальных данных. После создания ключа адрес локальных данных для каждого потока устанавливается равным NULL.

Кроме того, функция pthread_key_create может связать с вновь созданным ключом функцию-деструктор. Если адрес локальных данных при завершении потока имеет ненулевое значение, то вызывается функция-деструктор, которой в качестве аргумента передается адрес области памяти с локальными данными потока. Если в аргументе destructor передается пустой указатель, это означает, что для данного ключа не предусматривается вызов деструктора. Когда поток завершает работу вызовом функции pthread_exit или возвращает управление из своей стартовой процедуры, вызывается деструктор. Но если поток вызывает функцию exit, _exit, _Exit, abort или завершает работу аварийно, деструктор не вызывается.

Как правило, для выделения области памяти под свои локальные данные потоки используют функцию malloc. Функция-деструктор обычно освобождает эту область памяти. Если поток завершит работу без освобождения памяти, то эта область памяти будет потеряна для процесса.

Поток может создать несколько ключей для своих данных. Каждый ключ можно связать с деструктором. Это могут быть отдельные деструкторы для каждого из ключей или, наоборот, все ключи могут быть связаны с одной и той же функцией-деструктором. Операционная система может налагать свои ограничения на количество ключей, создаваемых в рамках одного процесса (PTHREAD_KEYS_MAX).

Порядок, в котором вызываются функции-деструкторы при завершении потока, не определен. В деструкторе допускается вызов функций, которые могут создавать новые локальные данные потока и связывать их с ключом. После вызова всех деструкторов система проверяет, не остались ли какие-либо непустые указатели на локальные данные потока, и если таковые будут обнаружены, деструкторы будут вызваны снова. Этот процесс повторяется до тех пор, пока не будут обнулены все указатели на локальные данные потока или не будет достигнуто максимально возможное количество итераций (PTHREAD_DESTRUCTOR_ITERATIONS).

Мы можем разорвать связь ключа с локальными данными для всех потоков, вызвав функцию pthread_key_delete.

#include <pthread.h>

int pthread_key_delete (pthread_key_t key);

/* функция возвращает 0 в случае успеха, код ошибки – в случае неудачи */

Обратите внимание, что вызов функции pthread_key_delete не приводит к вызову деструктора, связанного с ключом. Чтобы освободить память, занимаемую локальными данными потока, мы должны выполнить все необходимые для этого действия самостоятельно.

Размещая новый ключ, следует побеспокоиться о том, чтобы он не был изменен в процессе инициализации из другого потока. Использование кода, подобного приведенному ниже, может привести к тому, что функция pthread_key_create будет вызвана одновременно из нескольких потоков:

В зависимости от того, как система планирует выполнение потоков, одни потоки могут получить одно значение ключа, другие – другое. Решение проблемы заключается в использовании функции pthread_once.

#include <pthread.h>

pthread_once_t initflag = PTHREAD_ONCE_INIT;

int pthread_once (pthread_once_t *initflag, void (*initfn)(void));

/* функция возвращает 0 в случае успеха, код ошибки – в случае неудачи */

Параметр initflag должен быть глобальной или статической переменной, которой предварительно присвоено значение PTHREAD_ONCE_INIT.

Система гарантирует, что функция инициализации initfn будет вызвана не более одного раза при самом первом обращении к функции pthread_once, независимо от того, сколько раз вызывается функция pthread_once. Таким образом, правильный способ создания ключа выглядит следующим образом:

После того как ключ будет создан, его можно связать с локальными данными потока с помощью функции pthread_setspecific. Чтобы по заданному ключу получить адрес области памяти с локальными данными потока, следует обратиться к функции pthread_getspecific.

#include <pthread.h>

void *pthread_getspecific (pthread_key_t key);

/* функция возвращает указатель на область памяти с локальными данными или NULL, если ключ не связан с данными */

int pthread_setspecific (pthread_key_t key, void *value);

/* функция возвращает 0 в случае успеха, код ошибки – в случае неудачи */

Если с ключом не были связаны локальные данные потока, то функция pthread_getspecific вернет пустой указатель. Мы можем использовать это обстоятельство, чтобы определить, следует ли вызывать функцию pthread_setspecific.

Соседние файлы в папке Chapter.4