Threadsafe vs ré-entrant

Récemment, j’ai posé une question avec le titre “Le fil malloc est-il sûr?” , et à l’intérieur que j’ai demandé, “Est-ce que malloc re-entrant?”

J’avais l’impression que tous les ré-entrants étaient sécurisés pour les threads.

Cette hypothèse est-elle fausse?

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

Certaines fonctions nécessitent un emplacement pour stocker un «work in progress», les fonctions ré-entrantes vous permettent de spécifier ce pointeur dans le propre stockage du thread, et non dans un environnement global. Étant donné que ce stockage est exclusif à la fonction appelante, il peut être interrompu et saisi à nouveau (ré-entrant) et, dans la plupart des cas, l’exclusion mutuelle au-delà de ce que la fonction fil de sécurité . 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 le plus étrange dans toute explication du fonctionnement) 🙂

En bref, reentrant 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 ré-entrant (ou inversement). Lorsque vous examinez la sécurité des threads, vous devez réfléchir à la concurrence . Si vous devez fournir un moyen de locking et d’exclusion mutuelle pour utiliser une fonction, la fonction n’est pas insortingnsèquement sûre pour les threads.

Mais toutes les fonctions ne doivent pas non plus être examinées. malloc() n’a pas besoin d’être réentrant, il ne dépend d’aucune partie du périmètre du point d’entrée pour un thread donné (et est lui-même thread-safe).

Les fonctions qui renvoient des valeurs statiquement allouées ne sont pas sûres pour les threads sans l’utilisation d’un mutex, d’un futex ou d’un autre mécanisme de locking atomique. Cependant, ils n’ont pas besoin d’être réentrants s’ils ne sont pas 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; } 

Ainsi, comme vous pouvez le voir, le fait d’avoir plusieurs threads fait que, sans une sorte de locking, ce serait un désastre. Vous rencontrerez cela lorsque la mémoire allouée dynamicment est taboue sur certaines plates-formes embarquées.

En programmation purement fonctionnelle, le réentrant n’implique souvent pas la sécurité des threads, cela dépend du comportement des fonctions définies ou anonymes transmises au point d’entrée de la fonction, à la récursivité, etc.

Une meilleure façon de mettre «thread safe» est sûre pour les access concurrents , ce qui illustre mieux le besoin.

Cela dépend de la définition. Par exemple, Qt utilise les éléments suivants:

  • Une fonction thread-safe * peut être appelée simultanément à partir de plusieurs threads, même lorsque les invocations 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 invocation 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 adaptée aux threads.

Par extension, une classe est dite réentrante si ses fonctions membres peuvent être appelées en toute sécurité à partir de plusieurs threads, à condition 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 aussi en garde:

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

TL; DR: Une fonction peut être réentrante, sécurisée pour les threads, les deux ou aucun.

Les articles de Wikipedia sur la sécurité des fils 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 à n’importe quel moment de son exécution puis appelé de nouveau en toute sécurité (“re-enter”) avant que ses invocations précédentes ne se terminent.

En tant qu’exemples de réentrance possible, Wikipedia donne l’exemple d’une fonction conçue pour être appelée par les interruptions du système: supposons qu’elle fonctionne déjà 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 mono-thread si vous utilisez des fonctions de rappel ou récursives.

La clé pour éviter la confusion est que réentrant se réfère à un seul thread en cours d’exécution. C’est un concept à partir du moment où aucun système d’exploitation multitâche n’existait.

Exemples

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

Exemple 1: pas de thread-safe, pas de 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, pas 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: pas de 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; }