Еще немного экзотики языка С. С этим понятием я сам познакомился буквально в прошлом году, когда готовил прошлый цикл постов. В универе о таком не рассказывали (либо я забыл) а в работе не сталкивался. Тем не менее опытные программисты на C и C++ наверняка знают о нем и так или иначе учитывают его в системном программировании. Это понятие относится не только к конкуррентному программированию но и последовательному. Начнем с определения.
Реентерабельность (reentrancy) - свойство функции, при котором её можно безопасно вызывать повторно, даже если предыдущее выполнение ещё не закончилось.
Пример функции из стандартной библиотеки С
strtok - классической нереентерабельной функции: #include <stdio.h>
#include <string.h>
int main(void) {
char str[] = "one two three four";
const char *delim = " "; // разделитель — пробел
char *token = strtok(str, delim);
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, delim); // продолжаем с того места, где остановились
}
return 0;
}
Вывод:
one
two
three
four
Почему функция нереентерабельная? Она внутри себя хранит статическую переменную - состояние строки. Обратите внимание на код, в цикле мы не передаем ничего кроме разделителя то есть функция "запомнила" нашу строку.
Думаю вы догадались, использование такой функции для нескольких строк вызовет проблемы:
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "one,two,three";
char str2[] = "alpha,beta,gamma";
char *token;
token = strtok(str1, ",");
printf("%s\n", token);
token = strtok(str2, ",");
printf("%s\n", token);
// продолжение парсинга первой строки сломано
token = strtok(NULL, ",");
printf("%s\n", token);
}
Вывод:
one
alpha
beta
Наверняка вы как и я словили недоумение. Зачем в стандартной библиотека языка функцию реализовали через внутренний статический буфер? Ответ простой - C создавался в 70-х, задолго до эпохи повсеместных многопроцессорных систем / multithreading и гигабайтных оперативок. Тогда:
- память была ограничена;
- скорость была критична;
- программы были однопоточными.
Когда компьютеры стали мощнее, появились примитивы многопоточности встал вопрос - как писать код, с такими то библиотечными функциями? Ответ - создание реентерабельных функций - аналогов.
Пример кода для
strtok_r - реентерабельной реализации strtok. Она не хранит внутреннего состояния, её поведение регулируется аргументами, можно вызывать из нескольких потоков и все будет работать корректно.#include <stdio.h>
#include <string.h>
int main(void) {
char s1[] = "one,two,three";
char s2[] = "alpha,beta,gamma";
char *save1 = NULL;
char *save2 = NULL;
char *t1 = strtok_r(s1, ",", &save1);
printf("s1: %s\n", t1);
char *t2 = strtok_r(s2, ",", &save2);
printf("s2: %s\n", t2);
t1 = strtok_r(NULL, ",", &save1);
printf("s1: %s\n", t1);
t2 = strtok_r(NULL, ",", &save2);
printf("s2: %s\n", t2);
t1 = strtok_r(NULL, ",", &save1);
printf("s1: %s\n", t1);
t2 = strtok_r(NULL, ",", &save2);
printf("s2: %s\n", t2);
printf("s1: %s\n", strtok_r(NULL, ",", &save1));
printf("s2: %s\n", strtok_r(NULL, ",", &save2));
return 0;
}
Вывод:
s1: one
s2: alpha
s1: two
s2: beta
s1: three
s2: gamma
s1: (null)
s2: (null)
Обратите внимание на аргументы - теперь функция принимает буфер, чтобы ответственность за работу с памятью взял на себя программист. Хочешь простой код? Аллоцируй на каждый вызов функции новый буфер и не парься. Хочешь меньше аллокаций? Делай единый буфер, защищай его примитивом синхронизации если пишешь многопоточную программу.
Большая сила - большая ответственность👨🦳
Спасибо за то что читали, задавайте вопросы, рассказывайте сталкивались ли с реентерабельностью в работе. Поправляйте, дополняйте меня если что то упустил.