[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]

Программирование: Разработка и отладка программ


Написание реентерабельных программ и программ с защитой нитей

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

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

Основные сведения о реентерабельности и защите нитей

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

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

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

Реентерабельную функцию часто (но не всегда) можно определить по внешнему интерфейсу и по характеру применения. Например, функция strtok нереентерабельна, так как она хранит строку, разбиваемую на маркеры. Функция ctime также нереентерабельна, поскольку она возвращает указатель на статические данные, изменяемые при каждом вызове.

Защита нитей

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

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

/* функция с защитой нитей */
int diff(int x, int y)
{
        int delta;
 
        delta = y - x;
        if (delta < 0)
                delta = -delta;
 
        return delta;
}

Применение глобальных данных нарушает защиту нитей. Глобальные данные необходимо обрабатывать в рамках какой-либо одной нити или инкапсулировать для сериализации доступа к ним. Нить может считывать код ошибки из другой нити. В AIX у каждой нити собственное значение errno.

Создание реентерабельных функций

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

Возврат данных

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

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

/* нереентерабельная функция */
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;
}

Стандартные нереентерабельные библиотеки языка С были переработаны именно этим способом. Этот вопрос обсуждается в разделе Реентерабельные библиотеки с защитой нитей.

Хранение данных в промежутках между вызовами

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

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

/* нереентерабельная функция */
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;
}

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

/* реентерабельная функция */
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;
}

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

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;
}

Для обеспечения защиты нитей необходимо защитить статическую переменную counter статической блокировкой, как в следующем примере псевдокода:

/* псевдокод функции с защитой нитей */
int increment_counter();
{
        static int counter = 0;
        static lock_type counter_lock = LOCK_INITIALIZER;
 
        lock(counter_lock);
        counter++;
        unlock(counter_lock);
        return counter;
}

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

Способы безопасного применения функций без защиты нитей

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

Так как эти способы должны применяться только в приложениях, но не в библиотеках, для защиты библиотек можно применять взаимные блокировки.

Реентерабельные библиотеки с защитой нитей

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

Работа с библиотеками

Часть библиотек, входящих в комплект поставки Базовой операционной системы AIX, обеспечивают защиту нитей. В текущей версии AIX это следующие библиотеки:

Некоторые стандартные функции 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);

К библиотеке без защиты нитей может обращаться только одна нить программы. Ответственность за соблюдение этого правила несет разработчик программы; нарушение этого правила может привести к непредсказуемым результатам и даже сбою программы.

Преобразование библиотек

В этом разделе описаны основные этапы преобразования уже существующей библиотеки в реентерабельную библиотеку с защитой нитей. Содержимое этого раздела относится только к библиотекам языка С.

Связанная информация

Глава 9, Параллельное программирование

Рекомендации по созданию программ с нитями


[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]