Написание реентерабельных программ и программ с поддержкой нитей
В процессах с одной нитью только один поток управления, и обеспечивать реентерабельность кода или поддержку нитей не требуется. В процессах с несколькими нитями одни и те же функции и ресурсы могут использоваться несколькими потоками управления одновременно.
поэтому для обеспечения целостности ресурсов код должен быть реентерабельным и предусматривать поддержку нитей.
Оба понятия - реентерабельность и поддержка нитей - связаны со способом работы функций с ресурсами. Однако это различные свойства: функция может обладать одним из них, обоими или ни одним.
В этом разделе содержится информация о написании реентерабельных программ и программ с поддержкой нитей. В нем не обсуждаются вопросы повышения эффективности нитей, т.е. оптимального распараллеливания потоков. Добиться такой эффективности можно только за счет удачного алгоритма. Существующие программы с одной нитью можно преобразовать в эффективные программы с несколькими нитями, однако для этого потребуется полностью переработать алгоритм и переписать программу заново.
Реентерабельность
Реентерабельная функция не может ни хранить статические данные в промежутках между вызовами, ни возвращать указатель на статические данные. Все данные передаются из вызывающей функции. Реентерабельная функция не может вызывать нереентерабельную функцию.
Реентерабельную функцию часто (но не всегда) можно определить по внешнему интерфейсу и по характеру применения. Например, функция strtok нереентерабельна, так как она хранит строку, разбиваемую на маркеры. Функция ctime также нереентерабельна, поскольку она возвращает указатель на статические данные, изменяемые при каждом вызове.
Поддержка нитей
Функция с поддержкой нитей обеспечивает защиту общих ресурсов от одновременного доступа путем установки блокировок. Определить функцию с поддержкой нитей по внешнему интерфейсу невозможно, так как поддержка нитей реализуется только на уровне алгоритма.
/* функция с поддержкой нитей */
int diff(int x, int y)
{
int delta;
delta = y - x;
if (delta < 0)
delta = -delta;
return delta;
}Применение глобальных данных не обеспечивает поддержку нитей. Глобальные данные необходимо обрабатывать в рамках какой-либо одной нити или инкапсулировать для сериализации доступа к ним. Нить может считывать код ошибки из другой нити. В AIX у каждой нити собственное значение errno.
Создание реентерабельных функций
В большинстве случаев для преобразования нереентерабельной функции в реентерабельную достаточно переделать только интерфейс. Нереентерабельные функции не могут применяться в нескольких нитях одновременно. Кроме того, в некоторых нереентерабельных функциях невозможно обеспечить поддержку нитей.
Возврат данных
- Возвращать динамические данные. В этом случае освобождение памяти выполняется в вызывающей функции. Преимущество такого подхода заключается в том, что не нужно переделывать интерфейс. Однако при этом не гарантируется совместимость с остальными программами: при вызове измененной функции из программы с одной нитью программа не освободит память.
- Использовать память, выделенную в вызывающей функции. Это рекомендуемый подход, хотя он и требует переработки интерфейса.
/* нереентерабельная функция */
char *strtoupper(char *string)
{
static char buffer[MAX_STRING_SIZE];
int index;
for (index = 0; string[index]; index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0
return buffer;
}/* реентерабельная функция (неудачный вариант) */
char *strtoupper(char *string)
{
char *buffer;
int index;
/* необходима проверка наличия ошибок! */
buffer = malloc(MAX_STRING_SIZE);
for (index = 0; string[index]; index++)
buffer[index] = toupper(string[index]);
buffer[index] = 0
return buffer;
}/* реентерабельная функция (более удачный вариант) */
char *strtoupper_r(char *in_str, char *out_str)
{
int index;
for (index = 0; in_str[index]; index++)
out_str[index] = toupper(in_str[index]);
out_str[index] = 0
return out_str;
}Стандартные нереентерабельные библиотеки языка С были переработаны именно способом выделения памяти в вызывающей функции.
Хранение данных в промежутках между вызовами
Хранить данные в промежутках между вызовами нельзя, так как вызовы могут осуществляться из разных нитей. Если какие-либо данные (например, рабочий буфер или указатель) необходимо сохранить после возврата из функции, то их следует хранить в вызывающей функции.
/* нереентерабельная функция */
char lowercase_c(char *string)
{
static char *buffer;
static int index;
char c = 0;
/* сохранение строки при первом вызове */
if (string != NULL) {
buffer = string;
index = 0;
}
/* поиск строчной буквы */
for (; c = buffer[index]; index++) {
if (islower(c)) {
index++;
break;
}
}
return c;
}h
/* реентерабельная функция */
char reentrant_lowercase_c(char *string, int *p_index)
{
char c = 0;
/* инициализация не выполняется - она уже выполнена в вызывающей функции */
/* поиск строчной буквы */
for (; c = string[*p_index]; (*p_index)++) {
if (islower(c)) {
(*p_index)++;
break;
}
}
return c;
}
char *my_string;
char my_char;
int my_index;
...
my_index = 0;
while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
...
}Создание функций с поддержкой нитей
В программах с несколькими нитями все функции, вызываемые из нескольких нитей, должны обеспечивать поддержку нитей. Однако существуют способы вызова в таких программах функций без поддержки нитей. Нереентерабельные функции обычно не являются функциями с поддержкой нитей, но после переработки в реентерабельные часто становятся таковыми.
Блокировка общих ресурсов
/* функция без поддержки нитей */
int increment_counter()
{
static int counter = 0;
counter++;
return counter;
}
/* псевдокод функции с поддержкой нитей */
int increment_counter();
{
static int counter = 0;
static lock_type counter_lock = LOCK_INITIALIZER;
pthread_mutex_lock(counter_lock);
counter++;
pthread_mutex_unlock(counter_lock);
return counter;
}В приложении с несколькими нитями, работающем с библиотекой нитей, для сериализации доступа к общим ресурсам следует применять взаимные блокировки. При работе с независимыми библиотеками может потребоваться использовать их вне контекста нитей и потому устанавливать блокировки других типов.
Способы безопасного применения функций без поддержки нитей
- Первый способ - глобальная блокировка всей библиотеки.
Блокировка устанавливается при каждом обращении к библиотеке (т.е. при каждом вызове
библиотечной функции или при каждом обращении к библиотечной глобальной переменной).
Недостаток такого подхода заключается в возможном снижении
производительности, так как в любой момент времени к каким бы то ни
было средствам библиотеки сможет обращаться только одна нить. Описанный ниже способ можно применять только в случае, если обращения к библиотеке редки,
или же временно.
/* псевдокод! */ lock(library_lock); library_call(); unlock(library_lock); lock(library_lock); x = library_var; unlock(library_lock); - Второй способ - блокировка каждого отдельного компонента
библиотеки (т.е. каждой функции и каждой глобальной переменной) или группы компонентов. Этот вариант значительно более трудоемок, но не приводит к снижению производительности.
Так как эти способы должны
применяться только в приложениях, но не в библиотеках, для защиты
библиотек можно применять взаимные блокировки.
/* псевдокод! */ lock(library_moduleA_lock); library_moduleA_call(); unlock(library_moduleA_lock); lock(library_moduleB_lock); x = library_moduleB_var; unlock(library_moduleB_lock);
Реентерабельные библиотеки с поддержкой нитей
Реентерабельные библиотеки с поддержкой нитей применяются во многих параллельных (а также асинхронных) средах программирования, а не только при программировании нитей. Рекомендуется применять и создавать только функции, обладающие свойствами реентерабельности и поддержки нитей.
Работа с библиотеками
- Стандартная библиотека C (libc.a)
- Библиотека, обеспечивающая совместимость с Berkeley (libbsd.a)
Некоторые стандартные функции C, например, ctime и strtok, нереентерабельны. Имена реентерабельных версий этих функций отличаются суффиксом _r (знак подчеркивания и буква _r).
token[0] = strtok(string, separators);
i = 0;
do {
i++;
token[i] = strtok(NULL, separators);
} while (token[i] != NULL);char *pointer;
...
token[0] = strtok_r(string, separators, &pointer);
i = 0;
do {
i++;
token[i] = strtok_r(NULL, separators, &pointer);
} while (token[i] != NULL);К библиотеке без поддержки нитей может обращаться только одна нить программы. Ответственность за соблюдение этого правила несет разработчик программы; нарушение этого правила может привести к непредсказуемым результатам и даже сбою программы.
Преобразование библиотек
- Выявление экспортируемых глобальных переменных. Эти переменные обычно определяются в файлах заголовка с помощью ключевого слова export. Экспортируемые глобальные переменные необходимо инкапсулировать, т.е. сделать их закрытыми (объявить их в исходном коде библиотеки с помощью ключевого слова static) и создать для них операции чтения и записи.
- Выявление статических переменных и других общих ресурсов. Эти переменные обычно определяются с помощью ключевого слова static. Установка защиты посредством блокировок для всех общих ресурсов. Количество блокировок влияет на производительность библиотеки. Для инициализации блокировок можно воспользоваться функцией однократной инициализации.
- Выявление нереентерабельных функций и преобразование их в реентерабельные. Дополнительная информация приведена в разделе Создание реентерабельных функций.
- Выявление функций без поддержки нитей и преобразование их в функции с поддержкой нитей. Дополнительная информация приведена в разделе Создание функций с поддержкой нитей.