Threadsafe vs повторно въезжающий

Ответы:

42

Функции с повторным входом не полагаются на глобальные переменные, которые представлены в заголовках библиотеки C. Возьмите strtok () и strtok_r (), например, в C.

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

errno, однако, это немного другой случай в системах POSIX (и имеет тенденцию быть странным в любом объяснении того, как все это работает) :)

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

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

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

то есть:

static char *foo(unsigned int flags)
{
  static char ret[2] = { 0 };

  if (flags & FOO_BAR)
    ret[0] = 'c';
  else if (flags & BAR_FOO)
    ret[0] = 'd';
  else
    ret[0] = 'e';

  ret[1] = 'A';

  return ret;
}

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

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

Лучший способ сделать «потокобезопасным» - безопасный для одновременного доступа , что лучше иллюстрирует необходимость.

Тим Пост
источник
2
Реентерабельность не подразумевает потокобезопасность. Чистые функции подразумевают безопасность потоков.
Хулио Герра
Отличный ответ Тим. Чтобы прояснить, я понимаю из вашего «часто», что потокобезопасность не подразумевает реентерабельность, но и реентерабельность не подразумевает поточно-ориентированную. Сможете ли вы найти пример реентерабельной функции, которая не является потокобезопасной?
Riccardo
@ Tim Post «Короче говоря, реентерабельность часто означает потокобезопасность (например,« используйте реентерабельную версию этой функции, если вы используете потоки »), но поточная безопасность не всегда означает реентерабельность». qt говорит обратное: «Следовательно, потокобезопасная функция всегда является реентерабельной, но реентерабельная функция не всегда потокобезопасна».
4pie0
и википедия говорит еще кое-что: «Это определение повторного входа отличается от определения потоковой безопасности в многопоточных средах. Повторяющаяся подпрограмма может обеспечить потокобезопасность, [1] но одной реентерабельности может быть недостаточно для обеспечения потоковой безопасности во всех ситуациях. И наоборот, потокобезопасный код не обязательно должен быть реентерабельным (...) »
4pie0
@Riccardo: Функции, синхронизируемые с помощью изменчивых переменных, но не полных барьеров памяти для использования с обработчиками сигналов / прерываний, обычно реентерабельны, но ориентированы на потоки.
doynax
77

TL; DR: функция может быть реентерабельной, поточно-ориентированной, и той, и другой.

Стоит прочитать статьи в Википедии о безопасности потоков и повторном входе . Вот несколько цитат:

Функция является поточно-ориентированной, если:

он манипулирует только общими структурами данных таким образом, чтобы гарантировать безопасное выполнение несколькими потоками одновременно.

Функция является реентерабельной, если:

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

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

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

Примеры

(Немного изменено из статей Википедии)

Пример 1: не потокобезопасный, не реентерабельный

/* As this function uses a non-const global variable without
   any precaution, it is neither reentrant nor thread-safe. */

int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Пример 2: потокобезопасный, не реентерабельный

/* We use a thread local variable: the function is now
   thread-safe but still not reentrant (within the
   same thread). */

__thread int t;

void swap(int *x, int *y)
{
    t = *x;
    *x = *y;
    *y = t;
}

Пример 3: не потокобезопасный, реентерабельный

/* We save the global state in a local variable and we restore
   it at the end of the function.  The function is now reentrant
   but it is not thread safe. */

int t;

void swap(int *x, int *y)
{
    int s;
    s = t;
    t = *x;
    *x = *y;
    *y = t;
    t = s;
}

Пример 4: потокобезопасный, реентерабельный

/* We use a local variable: the function is now
   thread-safe and reentrant, we have ascended to
   higher plane of existence.  */

void swap(int *x, int *y)
{
    int t;
    t = *x;
    *x = *y;
    *y = t;
}
MiniQuark
источник
10
Я знаю, что я не должен комментировать, просто чтобы поблагодарить, но это одна из лучших иллюстраций, демонстрирующих различия между реентерабельными и потокобезопасными функциями. В частности, вы использовали очень краткие ясные термины и выбрали отличный пример функции, чтобы различать 4 категории. Так что спасибо!
ryyker
11
Мне кажется, что пример 3 не является реентерабельным: если обработчик сигнала, прерывающийся после t = *x, вызывает swap(), то tбудет переопределен, что приведет к неожиданным результатам.
rom1v 06
1
@ SandBag_1996, давайте рассмотрим вызов, swap(5, 6)который прерывается a swap(1, 2). После t=*x, s=t_originalи t=5. Теперь, после перерыва, s=5и t=1. Однако перед вторым swapвозвратом он восстановит контекст, сделав t=s=5. Теперь вернемся к первому swapс t=5 and s=t_originalи продолжим после t=*x. Итак, функция действительно повторяется. Помните, что каждый вызов получает свою собственную копию, sразмещенную в стеке.
urnonav
4
@ SandBag_1996 Предполагается, что если функция будет прервана (в любой момент), она будет вызываться снова, и мы ждем, пока она завершится, прежде чем продолжить исходный вызов. Если что-то еще происходит, то это в основном многопоточность, и эта функция не является потокобезопасной. Предположим, функция выполняет ABCD, мы принимаем только такие вещи, как AB_ABCD_CD, или A_ABCD_BCD, или даже A__AB_ABCD_CD__BCD. Как вы можете проверить, пример 3 будет нормально работать при этих предположениях, поэтому он является реентерабельным. Надеюсь это поможет.
MiniQuark
1
@ SandBag_1996, мьютекс фактически сделает его нереентерабельным. Первый вызов блокирует мьютекс. Приходит второй вызов - тупик.
urnonav
56

Это зависит от определения. Например, Qt использует следующее:

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

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

Следовательно, потокобезопасная функция всегда является реентерабельной, но реентерабельная функция не всегда потокобезопасна.

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

но они также предупреждают:

Примечание. Терминология в области многопоточности не полностью стандартизирована. POSIX использует определения реентерабельности и потокобезопасности, которые несколько отличаются для его C API. При использовании других объектно-ориентированных библиотек классов C ++ с Qt убедитесь, что определения понятны.

Георг Шёлли
источник
2
Это определение реентерабельности слишком строгое.
qweruiop
Функция является одновременно реентерабельной и поточно-ориентированной, если она не использует глобальную / статическую переменную. Потокобезопасность: когда много потоков запускают вашу функцию одновременно, есть ли какая-нибудь гонка? Если вы используете глобальную переменную, используйте блокировку для ее защиты. так что это потокобезопасный. реентерабельный: если во время выполнения вашей функции возникает сигнал и вы снова вызываете вашу функцию в сигнале, это безопасно ??? в таком случае нет нескольких потоков. Лучше всего не использовать статические / глобальные переменные, чтобы сделать его реентерабельным, или как в примере 3.
Кени ван