1. Comment est en toute sécurité défini ?
Sémantiquement. Dans ce cas, il ne s'agit pas d'un terme défini avec précision. Il signifie simplement "Vous pouvez faire cela, sans risque".
2. Si un programme peut être exécuté simultanément en toute sécurité, cela signifie-t-il toujours qu'il est réentrant ?
Non.
Par exemple, prenons une fonction C++ qui prend à la fois un verrou et un rappel en paramètre :
#include <mutex>
typedef void (*callback)();
std::mutex m;
void foo(callback f)
{
m.lock();
// use the resource protected by the mutex
if (f) {
f();
}
// use the resource protected by the mutex
m.unlock();
}
Une autre fonction pourrait bien avoir besoin de verrouiller le même mutex :
void bar()
{
foo(nullptr);
}
A première vue, tout semble correct Mais attendez :
int main()
{
foo(bar);
return 0;
}
Si le verrou sur le mutex n'est pas récursif, voici ce qui va se passer, dans le thread principal :
-
main
appellera foo
.
-
foo
va acquérir le verrou.
-
foo
appellera bar
qui appellera foo
.
- le 2e
foo
va essayer d'acquérir le verrou, échouer et attendre qu'il soit libéré.
- Impasse.
- Oups
Ok, j'ai triché, en utilisant le truc du callback. Mais il est facile d'imaginer des morceaux de code plus complexes ayant un effet similaire.
3. Quel est exactement le point commun entre les six points mentionnés que je dois garder à l'esprit lorsque je vérifie les capacités réentrantes de mon code ?
Vous pouvez odeur un problème si votre fonction a/donne accès à une ressource persistante modifiable, ou a/donne accès à une fonction qui odeurs .
( Ok, 99% de notre code devrait sentir, alors Voir la dernière section pour gérer ça )
Ainsi, en étudiant votre code, un de ces points devrait vous alerter :
- La fonction a un état (c'est-à-dire qu'elle accède à une variable globale, ou même à une variable de classe).
- Cette fonction peut être appelée par plusieurs threads, ou peut apparaître deux fois dans la pile pendant l'exécution du processus (c'est-à-dire que la fonction peut s'appeler elle-même, directement ou indirectement). Fonction prenant des callbacks comme paramètres odeur beaucoup.
Notez que la non-réentrance est virale : une fonction qui pourrait appeler une éventuelle fonction non-réentrante ne peut être considérée comme réentrante.
Notez également que les méthodes C++ odeur parce qu'ils ont accès à this
Il faut donc étudier le code pour s'assurer qu'ils n'ont pas d'interaction bizarre.
4.1. Toutes les fonctions récursives sont-elles réentrantes ?
Non.
Dans les cas multithreads, une fonction récursive accédant à une ressource partagée peut être appelée par plusieurs threads au même moment, ce qui entraîne des données erronées/corrompues.
Dans les cas monofilaires, une fonction récursive pourrait utiliser une fonction non récursive (comme la tristement célèbre strtok
), ou utiliser des données globales sans tenir compte du fait que ces données sont déjà utilisées. Ainsi, votre fonction est récursive car elle s'appelle elle-même directement ou indirectement, mais elle peut toujours être Récursif - non sûr .
4.2. Toutes les fonctions thread-safe sont-elles réentrantes ?
Dans l'exemple ci-dessus, j'ai montré comment une fonction apparemment threadsafe n'était pas réentrante. D'accord, j'ai triché à cause du paramètre callback. Mais alors, il y a de multiples façons de bloquer un thread en lui faisant acquérir deux fois un verrou non récurrent.
4.3. Toutes les fonctions récursives et thread-safe sont-elles réentrantes ?
Je dirais "oui" si par "récursif" vous entendez "sûr par récurrence".
Si vous pouvez garantir qu'une fonction peut être appelée simultanément par plusieurs threads, et peut s'appeler elle-même, directement ou indirectement, sans problème, alors elle est réentrante.
Le problème est d'évaluer cette garantie ^_^
5. Les termes tels que réentrée et sécurité des fils sont-ils absolus, c'est-à-dire ont-ils des définitions concrètes fixes ?
Je crois qu'ils le font, mais évaluer si une fonction est thread-safe ou réentrante peut être difficile. C'est pourquoi j'ai utilisé le terme odeur ci-dessus : Vous pouvez trouver qu'une fonction n'est pas réentrante, mais il peut être difficile d'être sûr qu'un morceau de code complexe est réentrant.
6. Un exemple
Disons que vous avez un objet, avec une méthode qui doit utiliser une ressource :
struct MyStruct
{
P * p;
void foo()
{
if (this->p == nullptr)
{
this->p = new P();
}
// lots of code, some using this->p
if (this->p != nullptr)
{
delete this->p;
this->p = nullptr;
}
}
};
Le premier problème est que si, d'une manière ou d'une autre, cette fonction est appelée de façon récursive (c'est-à-dire que cette fonction s'appelle elle-même, directement ou indirectement), le code se plantera probablement, car this->p
sera supprimé à la fin du dernier appel, et sera probablement encore utilisé avant la fin du premier appel.
Ainsi, ce code n'est pas récursif-sécurisé .
Nous pourrions utiliser un compteur de référence pour corriger cela :
struct MyStruct
{
size_t c;
P * p;
void foo()
{
if (c == 0)
{
this->p = new P();
}
++c;
// lots of code, some using this->p
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
}
};
De cette façon, le code devient récursif-sécurisé Mais il n'est toujours pas réentrant à cause des problèmes de multithreading : Nous devons être sûrs que les modifications de c
et de p
se fera de manière atomique, en utilisant un récursif mutex (tous les mutex ne sont pas récursifs) :
#include <mutex>
struct MyStruct
{
std::recursive_mutex m;
size_t c;
P * p;
void foo()
{
m.lock();
if (c == 0)
{
this->p = new P();
}
++c;
m.unlock();
// lots of code, some using this->p
m.lock();
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
m.unlock();
}
};
Et bien sûr, tout cela suppose que le lots of code
est lui-même ré-entrant, y compris l'utilisation de p
.
Et le code ci-dessus n'est pas du tout à l'abri des exceptions mais c'est une autre histoire ^_^
7. Hey 99% de notre code n'est pas réentrant !
C'est tout à fait vrai pour le code spaghetti. Mais si vous partitionnez correctement votre code, vous éviterez les problèmes de réentrance.
7.1. Assurez-vous que toutes les fonctions n'ont PAS d'état
Elles ne doivent utiliser que les paramètres, leurs propres variables locales, les autres fonctions sans état, et renvoyer des copies des données si elles en renvoient.
7.2. Assurez-vous que votre objet est "recursive-safe".
Une méthode objet a accès à this
Il partage donc un état avec toutes les méthodes de la même instance de l'objet.
Assurez-vous donc que l'objet peut être utilisé à un moment donné de la pile (c'est-à-dire en appelant la méthode A), puis à un autre moment (c'est-à-dire en appelant la méthode B), sans corrompre l'ensemble de l'objet. Concevez votre objet pour vous assurer qu'à la sortie d'une méthode, l'objet est stable et correct (pas de pointeurs qui pendent, pas de variables membres contradictoires, etc.)
7.3. Assurez-vous que tous vos objets sont correctement encapsulés
Personne d'autre ne devrait avoir accès à leurs données internes :
// bad
int & MyObject::getCounter()
{
return this->counter;
}
// good
int MyObject::getCounter()
{
return this->counter;
}
// good, too
void MyObject::getCounter(int & p_counter)
{
p_counter = this->counter;
}
Même le fait de renvoyer une référence const peut être dangereux si l'utilisateur récupère l'adresse des données, car une autre partie du code pourrait la modifier sans que le code détenant la référence const en soit informé.
7.4. Assurez-vous que l'utilisateur sait que votre objet n'est pas thread-safe.
Ainsi, l'utilisateur est responsable de l'utilisation des mutex pour utiliser un objet partagé entre les threads.
Les objets de la STL sont conçus pour ne pas être thread-safe (à cause de problèmes de performance), et donc, si un utilisateur veut partager un fichier std::string
entre deux threads, l'utilisateur doit protéger son accès avec des primitives de concurrence ;
7.5. Assurez-vous que votre code thread-safe est recursif-safe.
Cela signifie utiliser des mutex récursifs si vous pensez que la même ressource peut être utilisée deux fois par le même thread.
8 votes
En fait, je ne suis pas d'accord avec le point 2 de la première liste. Vous pouvez renvoyer une adresse vers ce que vous voulez à partir d'une fonction ré-entrante - la limitation est sur ce que vous faites avec cette adresse dans le code appelant.
2 votes
Mais comme l'auteur de la fonction réentrante ne peut pas contrôler ce que fait l'appelant, il ne doit pas renvoyer une adresse vers des données statiques (ou globales) non constantes pour que la fonction soit réellement réentrante.
3 votes
@drelihan Il n'est pas de la responsabilité de l'auteur de TOUTE fonction (réentrante ou non) de contrôler ce qu'un appelant fait avec une valeur retournée. Il doit certainement dire ce que l'appelant PEUT faire avec, mais si l'appelant choisit de faire autre chose - pas de chance pour l'appelant.
0 votes
"thread-safe" n'a aucun sens si vous ne spécifiez pas également ce que font les threads, et quel est l'effet attendu de leurs actions. Mais cela devrait peut-être faire l'objet d'une question distincte.
0 votes
J'entends par là que le comportement est bien défini et déterministe, indépendamment de la programmation.