Написание реентерабельных программ и программ с поддержкой нитей

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

поэтому для обеспечения целостности ресурсов код должен быть реентерабельным и предусматривать поддержку нитей.

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

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

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

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

Реентерабельную функцию часто (но не всегда) можно определить по внешнему интерфейсу и по характеру применения. Например, функция 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;
}

h

Эта функция нереентерабельна. Для того чтобы она стала реентерабельной, необходимо, чтобы статические данные, т.е. переменная 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;

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

Реентерабельные библиотеки с поддержкой нитей

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

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

Часть библиотек, входящих в комплект поставки Базовой операционной системы AIX, обеспечивают поддержку нитей. В текущей версии AIX это следующие библиотеки:
  • Стандартная библиотека 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. Установка защиты посредством блокировок для всех общих ресурсов. Количество блокировок влияет на производительность библиотеки. Для инициализации блокировок можно воспользоваться функцией однократной инициализации.
  • Выявление нереентерабельных функций и преобразование их в реентерабельные. Дополнительная информация приведена в разделе Создание реентерабельных функций.
  • Выявление функций без поддержки нитей и преобразование их в функции с поддержкой нитей. Дополнительная информация приведена в разделе Создание функций с поддержкой нитей.