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:
main
chiamerà foo
.
foo
acquisirà il blocco.
foo
chiamerà bar
, che chiameràfoo
.
- il 2o
foo
proverà ad acquisire il blocco, fallirà e attenderà che venga rilasciato.
- Deadlock.
- 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:
- La funzione ha uno stato (ovvero accesso a una variabile globale o persino a una variabile membro della classe)
- 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 c
e di p
verranno 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 code
sia 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.