1. Comment est défini en toute sécurité ?
Sémantiquement. Dans ce cas, ce n'est pas un terme bien défini. Cela signifie simplement "Vous pouvez le faire sans risque".
2. Si un programme peut être exécuté en toute sécurité simultanément, cela signifie-t-il toujours qu'il est réentrant?
Non.
Par exemple, ayons 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);
}
À première vue, tout semble ok… Mais attendez:
int main()
{
foo(bar);
return 0;
}
Si le verrou sur mutex n'est pas récursif, voici ce qui se passera, dans le thread principal:
main
va appeler foo
.
foo
va acquérir la serrure.
foo
appellera bar
, qui appellera foo
.
- le 2e
foo
tentera d'acquérir le verrou, échouera et attendra qu'il soit libéré.
- Impasse.
- Oups…
Ok, j'ai triché en utilisant le rappel. Mais il est facile d'imaginer des morceaux de code plus complexes ayant un effet similaire.
3. Quel est exactement le fil conducteur entre les six points mentionnés que je dois garder à l'esprit lors de la vérification de mes capacités réentrantes dans mon code?
Vous pouvez sentir un problème si votre fonction a / donne accès à une ressource persistante modifiable, ou a / donne accès à une fonction qui sent .
( Ok, 99% de notre code devrait sentir, alors… Voir la dernière section pour gérer ça… )
Ainsi, en étudiant votre code, l'un de ces points devrait vous alerter:
- La fonction a un état (c'est-à-dire accéder à une variable globale, ou même à une variable membre 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). Les fonctions prenant des rappels comme paramètres sentent beaucoup.
Notez que la non-réentrance est virale: une fonction qui pourrait appeler une éventuelle fonction non réentrante ne peut pas être considérée comme réentrante.
Notez également que les méthodes C ++ sentent parce qu'elles y ont accès this
, vous devez donc étudier le code pour vous assurer qu'elles n'ont pas d'interaction amusante.
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 en même temps, ce qui entraîne des données incorrectes / corrompues.
Dans les cas de thread unique, une fonction récursive peut utiliser une fonction non réentrante (comme l'infâme strtok
), ou utiliser des données globales sans gérer le fait que les données sont déjà utilisées. Votre fonction est donc récursive car elle s'appelle directement ou indirectement, mais elle peut toujours être non sécurisée récursive .
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. OK, j'ai triché à cause du paramètre de rappel. Mais alors, il existe plusieurs façons de bloquer un thread en lui faisant acquérir deux fois un verrou non récursif.
4.3. Toutes les fonctions récursives et thread-safe sont-elles réentrantes?
Je dirais «oui» si par «récursif» vous voulez dire «sûr récursif».
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 comme réentrance 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 alors, évaluer une fonction est thread-safe ou réentrant 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 cette fonction est appelée de manière récursive (c'est-à-dire que cette fonction s'appelle elle-même, directement ou indirectement), le code se bloquera probablement, car this->p
il sera supprimé à la fin du dernier appel, et sera probablement utilisé avant la fin du premier appel.
Par conséquent, ce code n'est pas récursif .
Nous pourrions utiliser un compteur de références 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 ... Mais il n'est toujours pas rentrant à cause de problèmes de multithreading: Nous devons être sûrs que les modifications de c
et de p
se feront atomiquement, en utilisant un mutex récursif (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 même pas protégé contre les exceptions à distance , mais c'est une autre histoire… ^ _ ^
7. Hé, 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
Ils doivent uniquement utiliser les paramètres, leurs propres variables locales, d'autres fonctions sans état et renvoyer des copies des données si elles reviennent.
7.2. Assurez-vous que votre objet est "récursif"
Une méthode objet a accès à this
, elle partage donc un état avec toutes les méthodes de la même instance de l'objet.
Donc, assurez-vous que l'objet peut être utilisé à un point de la pile (c'est-à-dire en appelant la méthode A), puis à un autre point (c'est-à-dire en appelant la méthode B), sans corrompre tout l'objet. Concevez votre objet pour vous assurer qu'à la sortie d'une méthode, l'objet est stable et correct (pas de pointeurs pendants, 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 renvoyer une référence const pourrait ê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 contenant la référence const ne soit informé.
7.4. Assurez-vous que l'utilisateur sait que votre objet n'est pas thread-safe
Ainsi, l'utilisateur est responsable d'utiliser des mutex pour utiliser un objet partagé entre les threads.
Les objets de la STL sont conçus pour ne pas être thread-safe (en raison de problèmes de performances), et donc, si un utilisateur souhaite partager un 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 récursif
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.