Quelle est exactement la fonction réentrante?

la plupart de le times , la définition de la rentrée est citée de Wikipedia :

un programme ou une routine d'ordinateur est décrit comme rentrant s'il peut être en toute sécurité a appelé de nouveau avant son l'invocation préc/" class="blnk">cédente est terminée (j'.e il peut être exécuté en toute sécurité simultanément.) Être réentrant, un programme d'ordinateur ou routine:

  1. Doit tenir aucun statique (ou global) données non constantes.
  2. ne doit pas retourner l'adresse à statique (ou globale) non constante données.
  3. doit fonctionner uniquement sur les données fournies par l'appelant.
  4. ne doit pas compter sur les serrures à singleton ressources.
  5. ne doit pas modifier son propre code (à moins exécuter dans son propre fil unique de stockage)
  6. ne doit pas appeler l'ordinateur Non-entrant programmes ou routines.

comment en toute sécurité défini?

si un programme peut être exécuté en toute sécurité en même temps , cela signifie-t-il toujours qu'il est rentrant?

Quel est exactement le point commun entre les six points mentionnés que je devrais garder à l'esprit lors de la vérification de mon code pour les capacités de rentrée?

aussi,

  1. Sont toutes les fonctions récursives réentrant?
  2. toutes les fonctions thread-safe rentrent-elles?
  3. est-ce que toutes les fonctions récursives et sans fil rentrent?

en écrivant cette question, une chose vient à l'esprit: Les termes comme rentrée et sécurité des fils sont-ils absolus, c'est-à-dire ont-ils des définitions de béton fixes? Car, s'ils ne le sont pas, cette question n'a pas beaucoup de sens.

166
demandé sur Community 2010-05-10 00:14:48

7 réponses

1. Comment est-il défini?

du point de vue Sémantique. Dans ce cas, ce n'est pas un dur-terme défini. Ça veut juste dire"Tu peux faire ça, sans risque".

2. Si un programme peut être exécuté concurremment en toute sécurité, cela signifie-t-il toujours qu'il est réentrant?

Pas de.

par exemple, nous allons avoir une fonction C++ qui prend à la fois une serrure et un rappel comme 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 la serrure sur mutex n'est pas récursive, alors voici ce qui va se passer, dans le fil principal:

  1. main appellera foo .
  2. foo acquerra l'écluse.
  3. foo appellera bar , qui appellera foo .
  4. le 2 foo essaiera d'acquérir la serrure, fail et d'attendre qu'elle soit libérée.
  5. impasse.
  6. Oups...

Ok, j'ai triché, en utilisant le truc du rappel. 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 devrais garder à l'esprit lors de la vérification de mon code pour les capacités de rentrée?

Vous pouvez odeur un problème si votre fonction a/donne accès à une modifiables ressources persistantes, 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 cela ... )

So, en étudiant votre code, un de ces points devrait vous alerter:

  1. la fonction a un État (i.e. accéder à une variable globale, ou même à une variable de membre de classe)
  2. cette fonction peut être appelée par plusieurs threads, ou peut apparaître deux fois dans la pile pendant que le processus est en cours d'exécution (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 fonction non-réentrante possible ne peut pas être considérée comme une réentrée.

Notez aussi que les méthodes C++ smell parce qu'ils ont accès à this , donc vous devriez étudier le code pour être sûr qu'ils n'ont pas d'interaction Drôle.

4.1. Sont toutes les fonctions récursives réentrant?

Pas de.

Dans les cas de multithread, une fonction récursive accédant à des ressources partagées pourrait être appelée par plusieurs threads au même moment, résultant en des données erronées/corrompues.

dans les cas simples, une fonction récursive pourrait utiliser une fonction non réentrante (comme infâme strtok ), ou utiliser des données globales sans traiter le fait que les données sont déjà utilisées. Ainsi votre fonction est récursive parce qu'elle s'appelle directement ou indirectement, mais elle peut encore être recursive-dangereux .

4.2. Est-ce que toutes les fonctions thread-safe rentrent?

dans l'exemple ci-dessus, j'ai montré comment une fonction apparemment threadsafe n'était pas rentrante. Ok, j'ai triché à cause du paramètre de rappel. Mais alors, il y a plusieurs façons de bloquer un fil en le faisant acquérir deux fois une serrure non récursive.

4.3. Est - ce que toutes les fonctions récursives et sans fil sont réentrées?

I vous diriez " Oui "si par" récursif "vous voulez dire"récursif-sûr".

si vous pouvez garantir qu'une fonction peut être appelée simultanément par plusieurs threads, et peut s'appeler, directement ou indirectement, sans problèmes, alors elle est de retour.

le problème est d'évaluer cette garantie ... "

5. Les Termes "rentrée" et "sécurité du fil" sont-ils absolument absolus, c'est-à-dire ont-ils des définitions concrètes fixes?

je crois qu'ils ont, mais alors, l'évaluation d'une fonction est thread-safe ou rentrante peut être difficile. C'est pourquoi j'ai utilisé le terme odeur ci-dessus: Vous pouvez trouver une fonction n'est pas réentrant, mais il pourrait être difficile d'être sûr d'un complexe morceau de code réentrant

6. Un exemple

disons que vous avez un objet, avec une méthode qui nécessitent l'usage des ressources:

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 le problème est que si d'une façon ou d'une autre cette fonction est appelée récursivement (c'est-à-dire que cette fonction s'appelle elle-même, directement ou indirectement), le code va probablement planter, parce que this->p sera supprimé à la fin du dernier appel, et sera encore probablement utilisé avant la fin du premier appel.

donc, ce code n'est pas recursive-safe .

nous pourrions utiliser un compteur de référence pour corriger ceci:

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ûr... mais il n'est toujours pas réentrant en raison de questions de lecture multiple: nous devons être sûrs que les modifications de c et de p seront faites atomiquement, en utilisant un recursive mutex (pas tous les mutex sont 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 rentrant, y compris l'utilisation de p .

et le le code ci-dessus n'est même pas distant exception-safe , mais c'est une autre histoire ... ^ _ ^

7. Hey 99% de notre code n'est pas reentrant!

c'est tout à fait vrai pour le code spaghetti. Mais si vous cloisonnez 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 ne doivent utiliser que les paramètres, leurs propres variables locales, autres fonctions sans état, et de retourner des copies des données si elles reviennent à tous.

7.2. Assurez-vous que votre objet est "recursive-sécuritaire"

une méthode objet a accès à this , de sorte qu'elle partage un état avec toutes les méthodes de la même instance de l'objet.

ainsi, assurez-vous que l'objet peut être utilisé à un point dans la pile( i.e. appelant la méthode A), et puis, à un autre point (i.e. appelant la méthode B), sans corrompre l'ensemble de l'objet. Concevez votre objet pour s'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 retourner une référence const pourrait être dangereux si l'utilisation récupère l'adresse des données, comme une autre partie du code pourrait le modifier sans que le code contenant la référence const soit indiqué.

7.4. Assurez-vous que l'utilisateur sait que vous Objet n'est pas thread-safe

Ainsi, l'utilisateur est responsable d'utiliser les mutex pour utiliser un objet partagé entre les threads.

les objets du STL sont conçus pour ne pas être thread-safe (en raison de problèmes de performance), et donc, si un utilisateur veut partager un std::string entre deux filetages, l'utilisateur doit protéger son accès avec des primitives de concurrence;

7.5. Assurez-vous que le code de sécurité du fil est récursif

cela signifie utiliser des Mutex récursifs si vous croyez que la même ressource peut être utilisée deux fois par le même thread.

167
répondu paercebal 2018-06-24 18:42:06

" en toute sécurité "est défini exactement comme le dicte le bon sens - cela signifie"faire correctement sa chose sans interférer avec d'autres choses". Les six points que vous citez expriment très clairement les exigences pour y parvenir.

les réponses à vos 3 questions sont 3×"non".


toutes les fonctions récursives sont-elles réentrées?

Non!

deux invocations simultanées d'une fonction récursive peuvent facilement se foirer l'une l'autre, si ils accèdent aux mêmes données globales / statiques, par exemple.


est-ce que toutes les fonctions thread-safe rentrent?

Non!

une fonction est thread-safe si elle ne dysfonctionne pas si elle est appelée concurremment. Mais ceci peut être réalisé par exemple en utilisant un mutex pour bloquer la exécution de la deuxième invocation jusqu'à la fin de la première, donc une seule invocation fonctionne à la fois. Réentrance signifie exécution simultanée sans interférence avec d'autres invocations .


est - ce que toutes les fonctions récursives et sans fil sont réentrées?

Non!

voir ci-dessus.

20
répondu slacker 2010-05-09 20:39:05

Le dénominateur commun:

le comportement est-il bien défini si la routine est appelée alors qu'elle est interrompue?

si vous avez une fonction comme celle-ci:

int add( int a , int b ) {
  return a + b;
}

elle ne dépend donc d'aucun état extérieur. Le comportement est bien définie.

si vous avez une fonction comme celle-ci:

int add_to_global( int a ) {
  return gValue += a;
}

Le résultat n'est pas bien défini sur plusieurs threads. L'Information pourrait être perdu si le timing était juste mauvais.

la forme la plus simple d'une fonction rentrante est quelque chose qui fonctionne exclusivement sur les arguments passés et les valeurs constantes. Autre chose prend une manutention ou, souvent, n'est pas réentrant. Et bien sûr, les arguments ne doivent pas faire référence à des globauxmutables.

10
répondu drawnonward 2010-05-09 20:50:22

maintenant je dois développer mon commentaire précédent. la réponse de @paercebal est incorrecte. Dans l'exemple de code, personne n'a remarqué que le mutex qui était supposé être le paramètre n'était pas passé?

je conteste la conclusion, j'affirme: pour qu'une fonction soit sûre en présence de concurrence, il faut qu'elle soit réentrée. Par conséquent, concurrent-safe (habituellement écrit thread-safe) implique re-entrant.

ni le fil de sécurité ni le réentrant n'ont tout à dire sur les arguments: nous parlons de l'exécution simultanée de la fonction, qui peut encore être dangereuse si des paramètres inappropriés sont utilisés.

par exemple, memcpy() est thread-safe et re-entrant (habituellement). De toute évidence, il ne fonctionnera pas comme prévu s'il est appelé avec des pointeurs vers les mêmes cibles à partir de deux threads différents. C'est le sens de la définition SGI, qui impose au client de s'assurer que les accès à la même structure de données sont synchronisés par le client.

il est important de comprendre qu'en général c'est non-sens d'avoir un fonctionnement sans fil comprenant les paramètres. Si vous avez fait une programmation de base de données, vous comprendrez. Le concept de ce qui est "atomique" et pourrait être protégé par un mutex ou une autre technique est nécessairement un concept d'utilisateur: le traitement d'une transaction sur une base de données peut nécessiter de multiples modifications non interrompues. Qui peut dire lesquels doivent être synchronisés? mais le programmeur client?

le point est que la" corruption " ne doit pas être gâcher la mémoire sur votre ordinateur avec écrit non numérisé: la corruption peut toujours se produire même si toutes les opérations individuelles sont sérialisées. Il s'ensuit que lorsque vous demandez si une fonction est thread-safe, ou re-entrant, la question signifie pour tous les arguments correctement séparés: l'utilisation d'arguments couplés ne constitue pas un contre-exemple.

il y a beaucoup systèmes de programmation: Ocaml est un, et je pense que Python, qui ont beaucoup de non réentrant code, mais qui utilise un verrou global pour interleave fil acesss. Ces systèmes ne sont pas re-entrant et ils ne sont pas thread-safe ou simultaneous-safe, ils fonctionnent en toute sécurité simplement parce qu'ils empêchent la concurrence à l'échelle mondiale.

un bon exemple est malloc. Il n'est pas re-entrant et pas Fil-safe. C'est parce qu'elle doit accéder à une ressource globale (le tas). Utilisation de serrures n'en est pas sûr: il n'est absolument pas réentrant. Si l'interface avec malloc avait été conçue correctement, il serait possible de la rendre réentrée et sans fil:

malloc(heap*, size_t);

maintenant, il peut être sûr parce qu'il transfère la responsabilité de sérialiser l'accès partagé à un tas unique au client. En particulier, aucun travail n'est nécessaire s'il existe des tas d'objets. Si un tas commun est utilisé, le client doit sérialiser l'accès. Utilisation d'une serrure à l'intérieur la fonction ne suffit pas: il suffit de considérer un malloc verrouillant un tas* et puis un signal arrive et appelle malloc sur le même pointeur: block: le signal ne peut pas continuer, et le client ne peut pas non plus parce qu'il est interrompu.

en général, les serrures ne rendent pas les choses filetage-sûr .. ils détruisent en fait la sécurité en essayant de gérer de façon inappropriée une ressource qui appartient au client. Le verrouillage doit être fait par l'objet du fabricant, c'est le seul code qui sait combien d'objets sont créés et comment ils seront utilisés.

7
répondu Yttrill 2010-12-10 05:36:26

the "common thread" (jeu de mots voulu!?) parmi les points énumérés, la fonction ne doit rien faire qui pourrait affecter le comportement d'appels récursifs ou concurrents à la même fonction.

ainsi, par exemple, les données statiques sont un problème parce qu'elles appartiennent à tous les threads; si un appel modifie une variable statique, tous les threads utilisent les données modifiées affectant ainsi leur comportement. Code modificateur autonome (bien que rarement rencontré, et dans certains cas prevented) poserait un problème, car bien qu'il y ait plusieurs threads, il n'y a qu'une copie du code; le code est aussi une donnée statique essentielle.

essentiellement pour être ré-entrant, chaque thread doit pouvoir utiliser la fonction comme s'il était le seul utilisateur, et ce n'est pas le cas si un thread peut affecter le comportement d'un autre d'une manière non déterministe. Cela implique principalement que chaque thread possède des données séparées ou constantes sur lesquelles la fonction fonctionne.

tout cela dit, le point (1) n'est pas nécessairement vrai; par exemple, vous pourriez légitimement et par conception utiliser une variable statique pour conserver un nombre de récursions pour se prémunir contre une récursion excessive ou pour profiler un algorithme.

une fonction sans fil ne doit pas nécessairement être rentrante; elle peut atteindre la sécurité du fil en empêchant spécifiquement la rentrée avec une serrure, et le point (6) indique qu'une telle fonction n'est pas rentrante. En ce qui concerne le point 6), une fonction qui appelle fonction thread-safe que locks n'est pas sûr pour une utilisation dans la récursion (il sera dead-lock), et n'est donc pas dit être rentrant, bien qu'il puisse néanmoins sûr pour la concurrence, et serait encore re-entrant dans le sens où plusieurs threads peuvent avoir leurs compteurs de programmes dans une telle fonction simultanément (juste pas avec la région verrouillée). Cela peut aider à distinguer la sécurité du fil de la réentarncy (ou peut-être ajoute à votre confusion!).

3
répondu Clifford 2010-05-09 20:47:46

les réponses à vos questions sont" non"," non "et" non". Juste parce qu'une fonction est récursive et/ou "thread-safe" ce n'est pas réentrant.

chacune de ces fonctions peut échouer sur tous les points que vous citez. (Bien que je ne sois pas sûr à 100% du point 5).

1
répondu ChrisF 2010-05-09 20:19:40

les Termes" Thread-safe "et" re-entrant " signifient seulement et exactement ce que leurs définitions disent. "Sûr "dans ce contexte signifie seulement ce que la définition que vous citez ci-dessous dit.

" sûr " ici ne signifie certainement pas sûr dans le sens large que l'appel d'une fonction donnée dans un contexte donné ne sera pas totalement flou votre application. Dans l'ensemble, une fonction pourrait produire de manière fiable un effet désiré dans votre application multi-threadée, mais ne pas qualifier comme rentrant ou fil-safe selon les définitions. À l'opposé, vous pouvez appeler des fonctions re-entrant de manière à produire une variété d'effets indésirables, inattendus et/ou imprévisibles dans votre application multi-threadée.

fonction récursive peut être n'importe quoi et re-entrant a une définition plus forte que thread-safe de sorte que les réponses à vos questions numérotées sont tout non.

en lisant la définition de rentrant, on pourrait résumer il s'agit d'une fonction qui ne modifiera rien au-delà de ce que vous appelez modifier. Mais il ne faut pas se fier uniquement au résumé.

Multi-threaded la programmation est juste extrêmement difficile dans le cas général. Savoir quelle partie du code réentrant est qu'une partie de ce défi. La sécurité du fil n'est pas additive. Plutôt que d'essayer de reconstituer les fonctions de rentrée, il est préférable d'utiliser un thread-safe design pattern et utilisez ce pattern pour guider votre utilisation de chaque thread et ressources partagées dans votre programme.

1
répondu Joe Soul-bringer 2010-05-10 01:19:13