Threadsafe vs réentrant

89

Récemment, j'ai posé une question, dont le titre était "Le fil malloc est-il sûr?" , et à l'intérieur, j'ai demandé: "Malloc est-il réentrant?"

J'avais l'impression que tous les rentrants sont thread-safe.

Cette hypothèse est-elle fausse?

Alphaneo
la source

Réponses:

42

Les fonctions rentrantes ne reposent pas sur des variables globales qui sont exposées dans les en-têtes de la bibliothèque C .. prenez strtok () vs strtok_r () par exemple en C.

Certaines fonctions ont besoin d'un endroit pour stocker un 'travail en cours', les fonctions réentrantes vous permettent de spécifier ce pointeur dans le propre stockage du thread, pas dans un global. Étant donné que ce stockage est exclusif à la fonction appelante, il peut être interrompu et réintégré (rentrant) et comme dans la plupart des cas, l'exclusion mutuelle au-delà de ce que la fonction implémente n'est pas nécessaire pour que cela fonctionne, ils sont souvent considérés comme étant thread safe . Ce n'est cependant pas garanti par définition.

errno, cependant, est un cas légèrement différent sur les systèmes POSIX (et a tendance à être étrange dans toute explication de la façon dont tout cela fonctionne) :)

En bref, réentrant signifie souvent thread-safe (comme dans «utiliser la version réentrante de cette fonction si vous utilisez des threads»), mais thread safe ne signifie pas toujours rentrant (ou l'inverse). Lorsque vous regardez la sécurité des threads, la concurrence est ce à quoi vous devez penser. Si vous devez fournir un moyen de verrouillage et d'exclusion mutuelle pour utiliser une fonction, la fonction n'est pas intrinsèquement thread-safe.

Mais toutes les fonctions n'ont pas besoin d'être examinées pour l'un ou l'autre. malloc()n'a pas besoin d'être réentrant, il ne dépend de rien hors de la portée du point d'entrée pour un thread donné (et est lui-même thread-safe).

Les fonctions qui renvoient des valeurs allouées statiquement ne sont pas sûres pour les threads sans l'utilisation d'un mutex, d'un futex ou d'un autre mécanisme de verrouillage atomique. Pourtant, ils n'ont pas besoin d'être réentrants pour ne pas être interrompus.

c'est à dire:

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

Donc, comme vous pouvez le voir, avoir plusieurs threads utiliser cela sans une sorte de verrouillage serait un désastre ... mais cela n'a aucun but d'être rentrant. Vous rencontrerez cela lorsque la mémoire allouée dynamiquement est taboue sur certaines plates-formes intégrées.

Dans la programmation purement fonctionnelle, réentrant n'implique souvent pas de thread safe, cela dépendrait du comportement de fonctions définies ou anonymes passées au point d'entrée de la fonction, de la récursivité, etc.

Une meilleure façon de mettre «thread safe» est sans danger pour l'accès simultané , ce qui illustre mieux le besoin.

Tim Post
la source
2
Réentrant n'implique pas de thread-safe. Les fonctions pures impliquent la sécurité des threads.
Julio Guerra
Bonne réponse Tim. Juste pour clarifier, ma compréhension de votre "souvent" est que thread-safe n'implique pas réentrant, mais aussi réentrant n'implique pas thread-safe. Seriez-vous capable de trouver un exemple de fonction réentrante qui n'est pas thread-safe?
Riccardo le
@ Tim Post "En bref, réentrant signifie souvent thread-safe (comme dans" utilisez la version réentrante de cette fonction si vous utilisez des threads "), mais thread safe ne signifie pas toujours rentrant." qt dit le contraire: "Par conséquent, une fonction thread-safe est toujours réentrante, mais une fonction réentrante n'est pas toujours thread-safe."
4pie0
et wikipedia dit encore quelque chose d'autre: «Cette définition de la réentrance diffère de celle de la sécurité des threads dans les environnements multithreads. Un sous-programme réentrant peut atteindre la sécurité des threads, [1] mais être réentrant seul peut ne pas être suffisant pour être thread-safe dans toutes les situations. Inversement, le code thread-safe ne doit pas nécessairement être réentrant (...) "
4pie0
@Riccardo: Les fonctions synchronisées via des variables volatiles mais pas des barrières de mémoire pleines à utiliser avec les gestionnaires de signaux / d'interruptions sont généralement rentrantes mais sûres pour les threads.
doynax
77

TL; DR: Une fonction peut être réentrante, thread-safe, les deux ou aucun.

Les articles de Wikipédia sur la sécurité des threads et la réentrance méritent d'être lus. Voici quelques citations:

Une fonction est thread-safe si:

il ne manipule que les structures de données partagées de manière à garantir une exécution sûre par plusieurs threads en même temps.

Une fonction est réentrante si:

il peut être interrompu à tout moment au cours de son exécution, puis rappelé en toute sécurité ("ressaisi") avant que ses appels précédents ne terminent l'exécution.

Comme exemples de réentrance possible, Wikipédia donne l'exemple d'une fonction conçue pour être appelée par des interruptions du système: supposons qu'elle soit déjà en cours d'exécution lorsqu'une autre interruption se produit. Mais ne pensez pas que vous êtes en sécurité simplement parce que vous ne codez pas avec des interruptions système: vous pouvez avoir des problèmes de réentrance dans un programme à un seul thread si vous utilisez des rappels ou des fonctions récursives.

La clé pour éviter toute confusion est que réentrant fait référence à un seul thread en cours d'exécution. C'est un concept de l'époque où il n'existait aucun système d'exploitation multitâche.

Exemples

(Légèrement modifié à partir des articles de Wikipedia)

Exemple 1: pas thread-safe, pas réentrant

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

Exemple 2: thread-safe, non réentrant

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

Exemple 3: non thread-safe, réentrant

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

Exemple 4: thread-safe, réentrant

/* 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
la source
10
Je sais que je ne suis pas censé commenter juste pour dire merci, mais c'est l'une des meilleures illustrations exposant les différences entre les fonctions réentrantes et thread-safe. En particulier, vous avez utilisé des termes clairs très concis et choisi un excellent exemple de fonction pour distinguer les 4 catégories. Donc merci!
ryyker
11
Il me semble que l'exemple 3 n'est pas réentrant: si un gestionnaire de signal, interrompant après t = *x, appelle swap(), alors tsera surchargé, conduisant à des résultats inattendus.
rom1v
1
@ SandBag_1996, considérons qu'un appel swap(5, 6)est interrompu par un swap(1, 2). Après t=*x, s=t_originalet t=5. Maintenant, après l'interruption, s=5et t=1. Cependant, avant le second swapretour, il restaurera le contexte, créant t=s=5. Maintenant, nous revenons au premier swapavec t=5 and s=t_originalet continuons après t=*x. Ainsi, la fonction semble être rentrante. N'oubliez pas que chaque appel obtient sa propre copie d' sallocation sur pile.
urnonav
4
@ SandBag_1996 L'hypothèse est que si la fonction est interrompue (à tout moment), elle ne doit être appelée qu'à nouveau, et nous attendons qu'elle se termine avant de continuer l'appel d'origine. Si quoi que ce soit d'autre se produit, il s'agit essentiellement de multithreading et cette fonction n'est pas thread-safe. Supposons que la fonction fasse ABCD, nous n'acceptons que des choses comme AB_ABCD_CD, ou A_ABCD_BCD, ou même A__AB_ABCD_CD__BCD. Comme vous pouvez le vérifier, l'exemple 3 fonctionnerait bien dans ces hypothèses, il est donc réentrant. J'espère que cela t'aides.
MiniQuark
1
@ SandBag_1996, le mutex le rendrait en fait non réentrant. Le premier appel verrouille le mutex. Dans vient la deuxième invocation - impasse.
urnonav
56

Cela dépend de la définition. Par exemple, Qt utilise ce qui suit:

  • Une fonction thread-safe * peut être appelée simultanément à partir de plusieurs threads, même lorsque les appels utilisent des données partagées, car toutes les références aux données partagées sont sérialisées.

  • Une fonction réentrante peut également être appelée simultanément à partir de plusieurs threads, mais uniquement si chaque appel utilise ses propres données.

Par conséquent, une fonction thread-safe est toujours réentrante, mais une fonction réentrante n'est pas toujours thread-safe.

Par extension, une classe est dite réentrante si ses fonctions membres peuvent être appelées en toute sécurité à partir de plusieurs threads, tant que chaque thread utilise une instance différente de la classe. La classe est thread-safe si ses fonctions membres peuvent être appelées en toute sécurité à partir de plusieurs threads, même si tous les threads utilisent la même instance de la classe.

mais ils mettent également en garde:

Remarque: la terminologie du domaine multithreading n'est pas entièrement standardisée. POSIX utilise des définitions de réentrant et de thread-safe qui sont quelque peu différentes pour ses API C. Lorsque vous utilisez d'autres bibliothèques de classes C ++ orientées objet avec Qt, assurez-vous que les définitions sont comprises.

Georg Schölly
la source
2
Cette définition de réentrant est trop forte.
qweruiop
Une fonction est à la fois réentrante et thread-safe si elle n'utilise aucune variable globale / statique. Thread - safe: lorsque plusieurs threads exécutent votre fonction en même temps, y a-t-il une course? Si vous utilisez global var, utilisez lock pour le protéger. donc il est thread-safe. réentrant: si un signal se produit pendant l'exécution de votre fonction, et que vous appelez à nouveau votre fonction dans le signal, est-ce sûr ??? dans ce cas, il n'y a pas de threads multiples. Il est préférable de ne pas utiliser de
variable