1. Wie ist sicher definiert?
Semantisch. In diesem Fall ist dies kein fest definierter Begriff. Es bedeutet nur "Sie können das ohne Risiko tun".
2. Wenn ein Programm sicher gleichzeitig ausgeführt werden kann, bedeutet dies immer, dass es wiedereintrittsfähig ist?
Nein.
Nehmen wir zum Beispiel eine C ++ - Funktion, die sowohl eine Sperre als auch einen Rückruf als Parameter verwendet:
#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();
}
Eine andere Funktion muss möglicherweise denselben Mutex sperren:
void bar()
{
foo(nullptr);
}
Auf den ersten Blick scheint alles in Ordnung zu sein ... Aber warte:
int main()
{
foo(bar);
return 0;
}
Wenn die Sperre für Mutex nicht rekursiv ist, geschieht Folgendes im Hauptthread:
main
wird anrufen foo
.
foo
wird das Schloss erwerben.
foo
wird anrufen bar
, die anrufen wird foo
.
- Der zweite
foo
versucht, die Sperre zu erlangen, schlägt fehl und wartet, bis sie freigegeben wird.
- Sackgasse.
- Hoppla…
Ok, ich habe mit dem Rückruf betrogen. Es ist jedoch leicht vorstellbar, dass komplexere Codeteile einen ähnlichen Effekt haben.
3. Was genau ist der rote Faden zwischen den sechs genannten Punkten, den ich beachten sollte, wenn ich meinen Code auf Wiedereintrittsmöglichkeiten überprüfe?
Sie können ein Problem riechen , wenn Ihre Funktion Zugriff auf eine veränderbare persistente Ressource hat oder Zugriff auf eine Funktion hat, die riecht .
( Ok, 99% unseres Codes sollten riechen, dann ... Siehe den letzten Abschnitt, um damit umzugehen ... )
Wenn Sie also Ihren Code studieren, sollte einer dieser Punkte Sie alarmieren:
- Die Funktion hat einen Status (dh Zugriff auf eine globale Variable oder sogar eine Klassenmitgliedsvariable)
- Diese Funktion kann von mehreren Threads aufgerufen werden oder während der Ausführung des Prozesses zweimal im Stapel angezeigt werden (dh die Funktion kann sich direkt oder indirekt selbst aufrufen). Funktion, die Rückrufe als Parameter nimmt, riecht viel.
Beachten Sie, dass Nicht-Wiedereintritt viral ist: Eine Funktion, die eine mögliche nicht-Wiedereintrittsfunktion aufrufen könnte, kann nicht als Wiedereintritt betrachtet werden.
Beachten Sie auch, dass C ++ - Methoden riechen, weil sie Zugriff darauf this
haben. Sie sollten daher den Code studieren, um sicherzustellen, dass sie keine lustige Interaktion haben.
4.1. Sind alle rekursiven Funktionen wiedereintrittsfähig?
Nein.
In Multithread-Fällen kann eine rekursive Funktion, die auf eine gemeinsam genutzte Ressource zugreift, von mehreren Threads gleichzeitig aufgerufen werden, was zu fehlerhaften / beschädigten Daten führt.
In Fällen mit Singuletthread kann eine rekursive Funktion eine nicht wiedereintretende Funktion (wie die berüchtigte strtok
) verwenden oder globale Daten verwenden, ohne die Tatsache zu behandeln, dass die Daten bereits verwendet werden. Ihre Funktion ist also rekursiv, weil sie sich direkt oder indirekt aufruft, aber dennoch rekursiv-unsicher sein kann .
4.2. Sind alle thread-sicheren Funktionen wiedereintrittsfähig?
Im obigen Beispiel habe ich gezeigt, dass eine scheinbar threadsichere Funktion nicht wiedereintrittsfähig ist. OK, ich habe wegen des Rückrufparameters betrogen. Es gibt jedoch mehrere Möglichkeiten, einen Thread zu blockieren, indem er zweimal eine nicht rekursive Sperre erhält.
4.3. Sind alle rekursiven und threadsicheren Funktionen wiedereintrittsfähig?
Ich würde "Ja" sagen, wenn mit "rekursiv" "rekursiv-sicher" gemeint ist.
Wenn Sie garantieren können, dass eine Funktion von mehreren Threads gleichzeitig aufgerufen werden kann und sich direkt oder indirekt problemlos aufrufen kann, ist sie wiedereintrittsfähig.
Das Problem ist die Bewertung dieser Garantie… ^ _ ^
5. Sind die Begriffe Wiedereintritt und Gewindesicherheit überhaupt absolut, dh haben sie feste konkrete Definitionen?
Ich glaube, dass sie es tun, aber dann kann die Bewertung einer Funktion threadsicher oder wiedereintrittsfähig sein. Aus diesem Grund habe ich oben den Begriff Geruch verwendet : Sie können feststellen, dass eine Funktion nicht wiedereintrittsfähig ist, es kann jedoch schwierig sein, sicherzustellen, dass ein komplexer Code wiedereintrittsfähig ist
6. Ein Beispiel
Angenommen, Sie haben ein Objekt mit einer Methode, die eine Ressource verwenden muss:
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;
}
}
};
Das erste Problem ist, dass, wenn diese Funktion irgendwie rekursiv aufgerufen wird (dh diese Funktion ruft sich direkt oder indirekt auf), der Code wahrscheinlich abstürzt, weil er this->p
am Ende des letzten Aufrufs gelöscht wird und wahrscheinlich noch vor dem Ende verwendet wird des ersten Anrufs.
Daher ist dieser Code nicht rekursiv sicher .
Wir könnten einen Referenzzähler verwenden, um dies zu korrigieren:
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;
}
}
};
Auf diese Weise wird der Code rekursiv sicher… Aufgrund von Multithreading-Problemen ist er jedoch immer noch nicht wiedereintrittsfähig: Wir müssen sicher sein, dass die Änderungen von c
und von p
atomar mithilfe eines rekursiven Mutex vorgenommen werden (nicht alle Mutexe sind rekursiv):
#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();
}
};
Und natürlich setzt dies alles voraus, dass lots of code
es selbst wiedereintritt, einschließlich der Verwendung von p
.
Und der obige Code ist nicht einmal ausnahmsweise ausnahmesicher , aber dies ist eine andere Geschichte… ^ _ ^
7. Hey, 99% unseres Codes sind nicht wiedereintrittsfähig!
Es ist ganz richtig für Spaghetti-Code. Wenn Sie Ihren Code jedoch korrekt partitionieren, vermeiden Sie Wiedereintrittsprobleme.
7.1. Stellen Sie sicher, dass alle Funktionen den Status NO haben
Sie dürfen nur die Parameter, ihre eigenen lokalen Variablen und andere Funktionen ohne Status verwenden und Kopien der Daten zurückgeben, wenn sie überhaupt zurückkehren.
7.2. Stellen Sie sicher, dass Ihr Objekt "rekursiv sicher" ist.
Eine Objektmethode hat Zugriff auf this
, sodass sie einen Status mit allen Methoden derselben Instanz des Objekts teilt.
Stellen Sie also sicher, dass das Objekt an einem Punkt im Stapel verwendet werden kann (dh Methode A aufrufen) und dann an einem anderen Punkt (dh Methode B aufrufen), ohne das gesamte Objekt zu beschädigen. Entwerfen Sie Ihr Objekt so, dass sichergestellt ist, dass das Objekt beim Beenden einer Methode stabil und korrekt ist (keine baumelnden Zeiger, keine widersprüchlichen Elementvariablen usw.).
7.3. Stellen Sie sicher, dass alle Ihre Objekte korrekt gekapselt sind
Niemand sonst sollte Zugriff auf seine internen Daten haben:
// 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;
}
Selbst die Rückgabe einer const-Referenz kann gefährlich sein, wenn der Benutzer die Adresse der Daten abruft, da ein anderer Teil des Codes diese ändern könnte, ohne dass der Code, der die const-Referenz enthält, darüber informiert wird.
7.4. Stellen Sie sicher, dass der Benutzer weiß, dass Ihr Objekt nicht threadsicher ist
Daher ist der Benutzer dafür verantwortlich, Mutexe zu verwenden, um ein Objekt zu verwenden, das von Threads gemeinsam genutzt wird.
Die Objekte aus der STL sind so konzipiert, dass sie (aufgrund von Leistungsproblemen) nicht threadsicher sind. Wenn ein Benutzer also std::string
zwei Threads gemeinsam nutzen möchte, muss der Benutzer seinen Zugriff mit Parallelitätsprimitiven schützen.
7.5. Stellen Sie sicher, dass Ihr thread-sicherer Code rekursiv ist
Dies bedeutet, dass Sie rekursive Mutexe verwenden, wenn Sie glauben, dass dieselbe Ressource zweimal von demselben Thread verwendet werden kann.