Che cos'è esattamente una funzione rientrante?


198

La maggior parte dei i tempi , la definizione di reentrance è citato da Wikipedia :

Un programma per computer o una routine viene descritto come rientrante se può essere richiamato in modo sicuro prima che sia stata completata la sua precedente chiamata (ovvero può essere eseguita in modo sicuro contemporaneamente). Per rientrare, un programma per computer o una routine:

  1. Non deve contenere dati non costanti statici (o globali).
  2. Non deve restituire l'indirizzo a dati statici (o globali) non costanti.
  3. Deve funzionare solo sui dati forniti dal chiamante.
  4. Non deve fare affidamento sui blocchi alle risorse singleton.
  5. Non deve modificare il proprio codice (a meno che non venga eseguito nel proprio archivio thread univoco)
  6. Non è necessario chiamare programmi o routine per computer non rientranti.

Come viene definito in modo sicuro ?

Se un programma può essere eseguito in sicurezza contemporaneamente , significa sempre che è rientrato?

Qual è esattamente il filo conduttore tra i sei punti citati che dovrei tenere a mente mentre controllo il mio codice per le capacità rientranti?

Anche,

  1. Tutte le funzioni ricorsive sono rientranti?
  2. Tutte le funzioni thread-safe sono rientranti?
  3. Tutte le funzioni ricorsive e thread-safe sono rientranti?

Durante la stesura di questa domanda, mi viene in mente una cosa: i termini come rientro e sicurezza dei thread sono assolutamente assoluti, ovvero hanno delle definizioni concrete? Perché, se non lo sono, questa domanda non è molto significativa.


6
In realtà, non sono d'accordo con il n. 2 nella prima lista. Puoi restituire un indirizzo a qualsiasi cosa ti piaccia da una funzione di rientro - la limitazione è su cosa fai con quell'indirizzo nel codice chiamante.

2
@Neil Ma come lo scrittore della funzione rientrante non può controllare ciò che sicuramente il chiamante non deve sicuramente restituire un indirizzo a dati statici (o globali) non costanti affinché esso sia effettivamente rientrante?
Robben_Ford_Fan_boy

2
@drelihan Non è responsabilità dello scrittore di QUALSIASI funzione (rientrante o meno) controllare cosa fa un chiamante con un valore restituito. Dovrebbero certamente dire cosa può fare il chiamante con esso, ma se il chiamante sceglie di fare qualcos'altro - sfortuna per il chiamante.

"thread-safe" non ha senso se non si specifica anche cosa stanno facendo i thread e quale sia l'effetto previsto delle loro azioni. Ma forse dovrebbe essere una domanda separata.

Prendo in sicurezza il significato, il comportamento è ben definito e deterministico indipendentemente dalla pianificazione.
AturSams,

Risposte:


191

1. Come è sicuro definito in ?

Semanticamente. In questo caso, questo non è un termine definito con precisione. Significa solo "Puoi farlo, senza rischi".

2. Se un programma può essere eseguito in sicurezza contemporaneamente, significa sempre che è rientrato?

No.

Ad esempio, disponiamo di una funzione C ++ che accetta sia un blocco sia un callback come parametro:

#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();
}

Un'altra funzione potrebbe aver bisogno di bloccare lo stesso mutex:

void bar()
{
    foo(nullptr);
}

A prima vista, sembra tutto ok ... Ma aspetta:

int main()
{
    foo(bar);
    return 0;
}

Se il blocco su mutex non è ricorsivo, ecco cosa accadrà, nel thread principale:

  1. main chiamerà foo .
  2. foo acquisirà il blocco.
  3. foochiamerà bar, che chiameràfoo .
  4. il 2o foo proverà ad acquisire il blocco, fallirà e attenderà che venga rilasciato.
  5. Deadlock.
  6. Spiacenti ...

Ok, ho tradito, usando la cosa di callback. Ma è facile immaginare pezzi di codice più complessi che abbiano un effetto simile.

3. Qual è esattamente il filo conduttore tra i sei punti citati che dovrei tenere a mente mentre controllo il mio codice per le capacità rientranti?

Puoi annusare un problema se la tua funzione ha / dà accesso a una risorsa persistente modificabile o ha / dà accesso a una funzione che ha un odore .

( Ok, il 99% del nostro codice dovrebbe avere un odore, quindi ... Vedi l'ultima sezione per gestirlo ... )

Quindi, studiando il tuo codice, uno di quei punti dovrebbe avvisarti:

  1. La funzione ha uno stato (ovvero accesso a una variabile globale o persino a una variabile membro della classe)
  2. Questa funzione può essere chiamata da più thread o potrebbe apparire due volte nello stack mentre il processo è in esecuzione (ovvero la funzione potrebbe chiamare se stessa, direttamente o indirettamente). La funzione accetta i callback poiché i parametri hanno un odore molto forte.

Notare che il non rientro è virale: una funzione che potrebbe chiamare una possibile funzione non rientrante non può essere considerata rientrante.

Nota anche che i metodi C ++ hanno un odore perché hanno accesso athis , quindi dovresti studiare il codice per essere sicuro che non abbiano interazioni divertenti.

4.1. Tutte le funzioni ricorsive sono rientranti?

No.

In casi multithread, una funzione ricorsiva che accede a una risorsa condivisa può essere chiamata da più thread contemporaneamente, causando dati danneggiati / danneggiati.

In casi con filetto singolo, una funzione ricorsiva potrebbe utilizzare una funzione non rientrante (come il famigerato strtok) o utilizzare dati globali senza gestire il fatto che i dati sono già in uso. Quindi la tua funzione è ricorsiva perché si chiama direttamente o indirettamente, ma può ancora essere ricorsiva e non sicura .

4.2. Tutte le funzioni thread-safe sono rientranti?

Nell'esempio sopra, ho mostrato come una funzione apparentemente thread-safe non fosse rientrata. OK, ho tradito a causa del parametro callback. Ma poi, ci sono diversi modi per deadlock di un thread facendolo acquisire due volte un blocco non ricorsivo.

4.3. Tutte le funzioni ricorsive e thread-safe sono rientranti?

Direi "sì" se per "ricorsivo" intendi "ricorsivo-sicuro".

Se puoi garantire che una funzione possa essere chiamata simultaneamente da più thread e possa chiamare se stessa, direttamente o indirettamente, senza problemi, allora è rientrante.

Il problema sta valutando questa garanzia ... ^ _ ^

5. I termini come rientro e sicurezza del thread sono assolutamente assoluti, ovvero hanno definizioni concrete fisse?

Credo che lo facciano, ma poi, valutare una funzione è thread-safe o rientrare può essere difficile. Questo è il motivo per cui ho usato il termine odore sopra: È possibile trovare una funzione non rientrante, ma potrebbe essere difficile essere certi che un codice complesso sia rientrante

6. Un esempio

Supponiamo che tu abbia un oggetto, con un metodo che deve usare una risorsa:

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

Il primo problema è che se in qualche modo questa funzione viene chiamata in modo ricorsivo (ovvero questa funzione si chiama da sola, direttamente o indirettamente), il codice probabilmente si arresterà in modo anomalo, perché this->p verrà eliminato alla fine dell'ultima chiamata e probabilmente verrà ancora usato prima della fine della prima chiamata.

Pertanto, questo codice non è ricorsivo .

Potremmo usare un contatore di riferimento per correggere questo:

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

In questo modo, il codice diventa ricorsivo e sicuro ... Ma non è ancora rientrante a causa di problemi di multithreading: dobbiamo essere sicuri che le modifiche di ce di pverranno eseguite atomicamente, usando un mutex ricorsivo (non tutti i mutex sono ricorsivi):

#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();
    }
};

E, naturalmente, tutto ciò presuppone che lots of codesia esso stesso rientrante, compreso l'uso di p.

E il codice sopra non è nemmeno lontanamente sicuro dalle eccezioni , ma questa è un'altra storia ... ^ _ ^

7. Ehi, il 99% del nostro codice non è rientrato!

È abbastanza vero per il codice spaghetti. Ma se si suddivide correttamente il codice, si eviteranno problemi di rientro.

7.1. Assicurarsi che tutte le funzioni non abbiano uno stato

Devono utilizzare solo i parametri, le proprie variabili locali, altre funzioni senza stato e restituire copie dei dati se ritornano affatto.

7.2. Assicurati che il tuo oggetto sia "ricorsivo-sicuro"

Un metodo oggetto ha accesso a this, quindi condivide uno stato con tutti i metodi della stessa istanza dell'oggetto.

Quindi, assicurati che l'oggetto possa essere usato in un punto dello stack (cioè chiamando il metodo A), e poi, in un altro punto (cioè chiamando il metodo B), senza corrompere l'intero oggetto. Progetta il tuo oggetto per assicurarti che all'uscita da un metodo, l'oggetto sia stabile e corretto (nessun puntatore penzolante, nessuna variabile membro contraddittoria, ecc.).

7.3. Assicurati che tutti i tuoi oggetti siano incapsulati correttamente

Nessun altro dovrebbe avere accesso ai propri dati interni:

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

Anche la restituzione di un riferimento const potrebbe essere pericolosa se l'utente recupera l'indirizzo dei dati, poiché un'altra parte del codice potrebbe modificarlo senza che sia stato detto il codice che contiene il riferimento const.

7.4. Assicurati che l'utente sappia che il tuo oggetto non è thread-safe

Pertanto, l'utente è responsabile dell'uso dei mutex per utilizzare un oggetto condiviso tra i thread.

Gli oggetti dell'STL sono progettati per non essere thread-safe (a causa di problemi di prestazioni) e, quindi, se un utente desidera condividere un std::string tra due thread, l'utente deve proteggere il suo accesso con primitive di concorrenza;

7.5. Assicurarsi che il codice thread-safe sia ricorsivo

Ciò significa utilizzare mutex ricorsivi se si ritiene che la stessa risorsa possa essere utilizzata due volte dallo stesso thread.


1
Per cavillare un po ', in realtà penso che in questo caso sia definita "sicurezza" - significa che la funzione agirà solo sulle variabili fornite - cioè, è una scorciatoia per la citazione di definizione sotto di essa. E il punto è che questo potrebbe non implicare altre idee di sicurezza.
Joe Soul-portatore

Ti sei perso passando nel mutex nel primo esempio?
detenere

@paercebal: il tuo esempio è sbagliato. In realtà non è necessario preoccuparsi del callback, una semplice ricorsione avrebbe lo stesso problema se ce n'è uno, tuttavia l'unico problema è che hai dimenticato di dire esattamente dove è allocato il blocco.
Yttrill,

3
@Yttrill: suppongo tu stia parlando del primo esempio. Ho usato il "callback" perché, in sostanza, un callback ha un odore. Naturalmente, una funzione ricorsiva avrebbe lo stesso problema, ma di solito si può facilmente analizzare una funzione e la sua natura ricorsiva, quindi rilevare se è rientrante o va bene per la ricorsività. Il callback, d'altra parte, significa che l'autore della funzione che chiama il callback non ha alcuna informazione su ciò che sta facendo il callback, quindi questo autore può avere difficoltà a assicurarsi che la sua funzione sia rientrata. Questa è questa difficoltà che volevo mostrare.
Paercebal,

1
@Gab 是 好人: ho corretto il primo esempio. Grazie! Un gestore di segnali verrebbe con i suoi problemi, distinti dalla reentranza, come di solito, quando viene generato un segnale, non si può davvero fare nulla oltre a cambiare una variabile globale dichiarata in modo specifico.
paercebal,

21

"Sicuro" è definito esattamente come il senso comune impone - significa "fare le sue cose correttamente senza interferire con altre cose". I sei punti citati esprimono chiaramente i requisiti per raggiungere questo obiettivo.

Le risposte alle tue 3 domande sono 3 × "no".


Tutte le funzioni ricorsive sono rientranti?

NO!

Due invocazioni simultanee di una funzione ricorsiva possono facilmente rovinarsi a vicenda, se ad esempio accedono agli stessi dati globali / statici.


Tutte le funzioni thread-safe sono rientranti?

NO!

Una funzione è thread-safe se non funziona male se chiamata contemporaneamente. Ma ciò può essere ottenuto, ad esempio, usando un mutex per bloccare l'esecuzione della seconda invocazione fino al termine della prima, quindi solo una chiamata alla volta funziona. Rientrare significa eseguire contemporaneamente senza interferire con altre invocazioni .


Tutte le funzioni ricorsive e thread-safe sono rientranti?

NO!

Vedi sopra.


10

Il filo conduttore:

Il comportamento è ben definito se la routine viene chiamata mentre viene interrotta?

Se hai una funzione come questa:

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

Quindi non dipende da alcuno stato esterno. Il comportamento è ben definito.

Se hai una funzione come questa:

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

Il risultato non è ben definito su più thread. Le informazioni potrebbero andare perse se i tempi fossero sbagliati.

La forma più semplice di una funzione rientrante è qualcosa che opera esclusivamente sugli argomenti passati e sui valori costanti. Qualsiasi altra cosa richiede una gestione speciale o, spesso, non rientra. E ovviamente gli argomenti non devono fare riferimento a globi mutabili.


7

Ora devo approfondire il mio commento precedente. La risposta @paercebal non è corretta. Nel codice di esempio nessuno ha notato che il mutex che, come si suppone fosse un parametro, non è stato effettivamente passato?

Contesto la conclusione, asserisco: affinché una funzione sia sicura in presenza di concorrenza deve essere rientrante. Pertanto la sicurezza concorrente (di solito scritta thread-safe) implica il rientro.

Né thread safe né re-entrant hanno nulla da dire sugli argomenti: stiamo parlando dell'esecuzione simultanea della funzione, che può comunque essere pericolosa se vengono utilizzati parametri inappropriati.

Ad esempio, memcpy () è thread-safe e rientra (di solito). Ovviamente non funzionerà come previsto se chiamato con puntatori agli stessi target da due thread diversi. Questo è il punto della definizione SGI, ponendo l'onere sul client per garantire che gli accessi alla stessa struttura di dati siano sincronizzati dal client.

È importante comprendere che in generale non ha senso avere un funzionamento sicuro dei thread che include i parametri. Se hai programmato un database, capirai. Il concetto di ciò che è "atomico" e potrebbe essere protetto da un mutex o da qualche altra tecnica è necessariamente un concetto utente: l'elaborazione di una transazione su un database può richiedere più modifiche senza interruzioni. Chi può dire quali devono essere sincronizzati se non il programmatore client?

Il punto è che la "corruzione" non deve rovinare la memoria del tuo computer con scritture non serializzate: la corruzione può ancora verificarsi anche se tutte le singole operazioni sono serializzate. Ne consegue che quando si chiede se una funzione è thread-safe o rientra, la domanda significa per tutti gli argomenti opportunamente separati: l'utilizzo di argomenti accoppiati non costituisce un contro-esempio.

Esistono molti sistemi di programmazione: Ocaml è uno, e penso anche Python, che contiene un sacco di codice non rientrante, ma che utilizza un blocco globale per intercalare gli accessi ai thread. Questi sistemi non rientrano e non sono thread-safe o simultaneamente sicuri, funzionano in modo sicuro semplicemente perché impediscono la concorrenza a livello globale.

Un buon esempio è malloc. Non è rientrante e non è sicuro per i thread. Questo perché deve accedere a una risorsa globale (l'heap). L'uso dei lucchetti non lo rende sicuro: sicuramente non rientra. Se l'interfaccia per malloc fosse stata progettata correttamente, sarebbe possibile renderlo rientrante e sicuro per i thread:

malloc(heap*, size_t);

Ora può essere sicuro perché trasferisce al client la responsabilità di serializzare l'accesso condiviso a un singolo heap. In particolare, non è necessario alcun lavoro se sono presenti oggetti heap separati. Se viene utilizzato un heap comune, il client deve serializzare l'accesso. L'uso di un blocco all'interno della funzione non è sufficiente: basta considerare un malloc che blocca un heap * e quindi arriva un segnale e chiama malloc sullo stesso puntatore: deadlock: il segnale non può procedere e il client non può neanche perché è interrotto.

In generale, i blocchi non rendono le cose thread-safe .. in realtà distruggono la sicurezza cercando in modo inappropriato di gestire una risorsa di proprietà del client. Il blocco deve essere eseguito dal produttore dell'oggetto, questo è l'unico codice che sa quanti oggetti vengono creati e come verranno utilizzati.


"Pertanto la sicurezza concorrente (di solito scritta thread-safe) implica il rientro." Ciò contraddice l' esempio di Wikipedia "Thread-safe ma non rientrante" .
Maggyero,

3

Il "thread comune" (gioco di parole !?) tra i punti elencati è che la funzione non deve fare nulla che possa influenzare il comportamento di qualsiasi chiamata ricorsiva o simultanea alla stessa funzione.

Quindi, ad esempio, i dati statici sono un problema perché sono di proprietà di tutti i thread; se una chiamata modifica una variabile statica, tutti i thread usano i dati modificati, influenzando così il loro comportamento. Il codice di auto-modifica (anche se raramente riscontrato, e in alcuni casi prevenuto) sarebbe un problema, perché sebbene ci siano più thread, esiste solo una copia del codice; il codice è anche un dato statico essenziale.

Essenzialmente per rientrare, ogni thread deve essere in grado di usare la funzione come se fosse l'unico utente, e non è così se un thread può influenzare il comportamento di un altro in modo non deterministico. Principalmente ciò implica che ogni thread abbia dati separati o costanti su cui la funzione lavora.

Detto questo, il punto (1) non è necessariamente vero; ad esempio, è possibile legittimamente e, in base alla progettazione, utilizzare una variabile statica per conservare un conteggio di ricorsione per evitare ricorsioni eccessive o per profilare un algoritmo.

Non è necessario rientrare in una funzione thread-safe; può raggiungere la sicurezza del filo impedendo in modo specifico il rientro con un lucchetto, e il punto (6) afferma che tale funzione non è rientrante. Per quanto riguarda il punto (6), una funzione che chiama una funzione thread-safe che blocca non è sicura per l'uso in ricorsione (si bloccherà in modo morto) e pertanto non si dice che sia rientrante, sebbene possa comunque essere sicura per la concorrenza, e sarebbe comunque rientrante, nel senso che più thread possono avere i loro contatori di programmi in una tale funzione contemporaneamente (ma non con la regione bloccata). Può essere che ciò aiuti a distinguere la sicurezza del thread dalla reincarnazione (o forse aumenti la tua confusione!).


1

Le risposte alle tue domande "Anche" sono "No", "No" e "No". Solo perché una funzione è ricorsiva e / o thread-safe, non la rende rientrante.

Ognuno di questi tipi di funzioni può fallire su tutti i punti citati. (Anche se non sono sicuro al 100% del punto 5).


1

I termini "thread-safe" e "re-entrant" significano solo ed esattamente ciò che dicono le loro definizioni. "Sicuro" in questo contesto significa solo ciò che dice la definizione che citi sotto.

"Sicuro" qui certamente non significa sicuro in senso lato che chiamare una determinata funzione in un determinato contesto non annulla totalmente l'applicazione. Nel complesso, una funzione può produrre in modo affidabile un effetto desiderato nella propria applicazione multi-thread ma non può essere classificata come rientrante o protetta da thread in base alle definizioni. Al contrario, è possibile chiamare le funzioni di rientro in modi che produrranno una varietà di effetti indesiderati, imprevisti e / o imprevedibili nella propria applicazione multi-thread.

La funzione ricorsiva può essere qualsiasi cosa e Re-entrant ha una definizione più forte di thread-safe, quindi le risposte alle tue domande numerate sono tutte no.

Leggendo la definizione di rientro, si potrebbe riassumere come significato una funzione che non modificherà nulla al di là di ciò che si chiama per modificarlo. Ma non dovresti fare affidamento solo sul riepilogo.

La programmazione multi-thread è semplicemente estremamente difficile nel caso generale. Sapere quale parte del proprio codice rientrare è solo una parte di questa sfida. La sicurezza del thread non è additiva. Piuttosto che provare a mettere insieme le funzioni rientranti, è meglio usare un modello di progettazione sicuro per i thread e utilizzare questo modello per guidare l'uso di ogni thread e risorse condivise nel programma.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.