std::list thread_safety
- Ich habe eine Liste, wo man thread nicht einfach push_back und anderen thread gelegentlich eine Schleife durch die Liste und druckt alle Elemente. Muss ich eine Sperre in diesem Fall?
-
Habe ich die Zeiger auf die Elemente in einem anderen Objekt. Ist es sicher zu haben? Ich weiß, dass Vektor verschieben Sie alle Objekte, wenn es mehr Speicherplatz benötigt, so Zeiger ungültig wird.
mylist.push_back(MyObj(1));
wenn(someCond)
{
_myLastObj = &mylist.back();
}
_myLastObj
ist der Typ MyObj*
Wenn ich hatte einen Vektor, das Objekt hätte an einen anderen Speicherort verschoben und der Zeiger würde zeigen, zu Müll. Ist es sicher mit einer Liste?
- Es ist nicht thread-sicher. 1). Ja, Sie sollten! 2) verstehe ich nicht. Klären ein wenig.
- möglich, Duplikat der muss ich schützen Lesen Sie Zugang zu einem STL-container in einer multithreading-Umgebung?
- Auch er mutiert es in einzelnen Fällen. Kein dupe.
Du musst angemeldet sein, um einen Kommentar abzugeben.
std::list
sind ungültig, nur wenn das selbe element aus der Liste entfernt. Da Ihr code nicht entfernt alles von der Liste, Ihre Zeiger sind sicher.Für einen konkreten Grund, warum Sie müssen die Sperre, denken Sie zum Beispiel an, dass
list::push_back
ist zulässig, um die folgenden, in dieser Reihenfolge:Wenn Ihr reader thread kommt zwischen 2 und 3, dann wird es gehen aus der vorherigen Schwanz, um den neuen Knoten, dann versuchen zu Folgen, einen nicht initialisierten Zeiger. Boom.
Im Allgemeinen jedoch, müssen Sie die Synchronisierung, weil es der einzige Weg, um zu garantieren, dass änderungen in der writer-thread veröffentlicht, um die Leser-thread in irgendeiner Art sinnvoll, um (oder auf allen).
Ihr code sollte korrekt sein, wenn Sie sich vorstellen, dass verschiedene threads auf verschiedenen Planeten, jeder mit seiner eigenen Kopie von Ihrem Programm den Speicher, und übertragen von änderungen auf jedes andere (a) wenn Sie eine Synchronisierung-Objekts (oder der atomic-variable in C++11), plus (b) wenn Sie nicht verwenden ein Synchronisations-Objekt, aber die übertragung bestimmten teilweisen änderung Willen zu brechen, Ihren code (wie die eine Hälfte eines zwei-Wort-Objekt oder in diesem Fall eine der beiden Zeiger schreibt, dass Sie auftreten müssen, um in einer bestimmten Reihenfolge). Manchmal dieses Modell ist konservativer, als es unbedingt nötig ist, und führt zu einer langsameren code. Aber eine weniger konservative Modell setzt auf die Umsetzung-konkrete details zu den threading-system und Speicher-Modell.
boost::s_list
momentan nichts von der Art, dann ist das dein Bier. Die Allgemeine Antwort ist, dass es unsicher. Wenn das, was Sie wollen, ist ein lock-free queue, für einen suchen, aberlist
unds_list
nicht.list
werden so geschrieben, dass Ihr code funktioniert. Es doesn ' T gewährleisten Sie, sodass Ihr code ist nicht sicher.Ich wollte herausfinden, ob die Listen wurden thread-sicher im Allgemeinen, so fragte ich, und diesen thread gefunden. Die Schlussfolgerung, die ich habe, ist, dass, in der aktuellen Implementierung von std::list in gcc libstdc++, können Sie sicher ändern Sie die Liste in einen thread und beliebig iterieren durch Listen gleichzeitig, aber wage es nicht zwei threads ändern der gleichen Liste (ohne syncronization). Darüber hinaus ist dieses Verhalten sollte nicht werden, abhängt. Ich habe Riss der code der Bibliothek darauf hin, die Probleme in etwas mehr detail. Ich hoffe, dass es hilfreich ist.
Zuerst beginnen wir mit der Allgemeinen Frage des thread-Sicherheit für Listen. Ich dachte, es wäre schön, zu "beweisen", dass die Listen, die unsicher sind, durch das Beispiel, so warf ich den folgenden code zusammen.
Die Ausgabe, die ich bekam war:
Huch. Das bedeutet, dass kaum Elemente, schob ich landete auf der Liste. Aber es ist schlimmer als das! Es ist nicht nur nicht das Recht haben, die Anzahl der Elemente, es denkt, es hat eine andere Nummer, als es eigentlich tut, und dass die Anzahl ist nicht das, was ich möchte! Während das bedeutet, dass es fast schon ein Speicher-Leck, bei mir lief es mit valgrind die Operationen erfolgreich abgeschlossen. Ich habe gehört, dass valgrind und andere tools können weniger, als hilfreich, wenn Sie versuchen umzugehen mit Parallelität, ich denke, dies ist der Beweis dafür.
Zuerst habe ich versucht, schieben 10 Elemente gleichzeitig oder so, aber passiert ist nichts faul. Ich dachte mir, dass es die Verwaltung zu schieben, alles in seiner time slice, also ich konnte es auf 10000 und bekam die Ergebnisse, die ich wollte. Nur ein Hinweis für jeden, der versucht zu duplizieren das experiment, es kann funktionieren oder auch nicht-je nach system-Konfiguration und scheduling-Algorithmus, etc.
Angesichts der Art der verknüpften Listen, ich hatte erwartet, ein solches experiment in einem seg-fault oder sonst beschädigt Liste. Ein seg-fault, wäre eine Gnade, wenn dies war der Grund für einige Fehler, die Sie gesucht haben.
Was zum Teufel ist Los
Hier werde ich erklären, was genau passiert ist und warum (oder zumindest eine sehr plausible Erklärung). Wenn Sie noch nicht initialisiert sind, um concurrency-Probleme, sollten Sie diese ein intro. Wenn Sie ein Experte sind, bitte sagen Sie mir, wo bin ich falsch oder unvollständig sind.
War ich neugierig, so dass ich schaute auf die gcc libstdc++ - Implementierung. Um zu erklären das beobachtete Verhalten, eine kurze Erklärung, wie die Liste funktioniert, ist in Ordnung.
Implementierungsdetails
Da ist überhaupt nichts Interessantes oder seltsames über die zugrunde liegende Struktur oder Algorithmus, aber es gibt verschiedene C++ - Implementierung-details, die erwähnt werden müssen. Zunächst werden die Knoten der Liste alle entspringen einer gemeinsamen Basis-Klasse, die speichert nur zwei Zeiger. Auf diese Weise, alle die das Verhalten der Liste gekapselt ist. Der eigentliche Knoten, die andere als die sich aus der Basis, sind Strukturen mit nicht-statischen Daten-member
__gnu_cxx::__aligned_membuf<_Tp> _M_storage
. Diese Knoten sind bewusst dievalue_type
von der Liste, und leiten sich von derallocator_type
rebound zu_List_node<_Tp>
. Der Zweck dieser Knoten ist zu erhalten und release-Speicher für die Liste, und verwenden Sie Ihre Basis zu pflegen, die Struktur der Daten. (Ich empfehle dieses Papier für eine Erklärung, wie die Typen sind ausgeklammert für die Benutzung von Iteratoren, es kann etwas Licht auf, warum bestimmte Dinge so umsetzen, wie Sie sind http://www.stroustrup.com/SCARY.pdf. Für die masochistische, sehen Sie dieses Assistenten erklären die schönen Alptraum, der c++ - allocators https://www.youtube.com/watch?v=YkiYOP3d64E). Die Liste kümmert sich dann um Aufbau und Zerstörung, und stellt die Schnittstelle für die Benutzer der Bibliothek, blah, blah, blah.Einen großen Vorteil, dass eine gemeinsame (Typ-ignorant) Basis-Klasse für die Knoten, die Sie haben können beliebige Knoten miteinander verbunden. Dies ist nicht sehr hilfreich, wenn man mit tollkühner, aber Sie nutzen es in einer kontrollierten Art und Weise. Die "Schwanz-Knoten" ist nicht vom Typ
value_type
, aber der Typsize_t
. Die Schwanz-Knoten nimmt die Größe der Liste! (Es dauerte ein paar Minuten, um herauszufinden, was Los war, aber es war Spaß, also keine große Sache. Der große Vorteil dabei ist, dass jede Liste in Existenz haben kann, die gleiche Art von Schwanz-Knoten, so gibt es weniger code-Duplizierung für den Umgang mit der Rute Knoten und die Liste braucht nur eine nicht-statischen Daten-member zu tun, was es tun muss).So, wenn ich push einen Knoten auf der Rückseite der Liste, die
end()
iterator übergeben, die folgende Funktion:_M_create_node()
schließlich verwendet die direkt-Zuweisung zu bekommen, der Speicher für die Knoten dann versucht, die Konstruktion eines Elements mit den Argumenten. Der "Punkt" des_M_hook()
Funktion ist zu zeigen, Zeiger auf Zeiger, auf die Sie zeigen soll, und ist hier aufgeführt:Die Reihenfolge, in der die Zeiger manipuliert werden, ist wichtig. Es ist der Grund, warum ich behaupten, dass Sie Durchlaufen können, während die Manipulation der Liste gleichzeitig. Mehr dazu später. Dann die Größe angepasst wird:
Wie gesagt, die Liste hat einen Schwanz Knoten vom Typ
size_t
, so, wie Sie sich vorstellen können,_M_impl._M_node._M_valptr()
ruft einen Zeiger auf diese Zahl, und dann+=
's es die richtige Menge.Das Beobachtete Verhalten
Also, was ist passiert? Die threads sind der Einstieg in eine Daten Rennen (https://en.cppreference.com/w/cpp/language/memory_model) in der
_M_hook()
und_M_inc_size()
Funktionen. Ich kann nicht finden, ein schönes Bild online, so sagen wir, dassT
ist der Schwanz,B
ist der "zurück," und wir wollen, schieben1
auf der Rückseite. Das heißt, wir haben die Liste (fragment)B <-> T
, und wir wollenB <-> 1 <-> T
. Schließlich1
Anrufe_M_hook
aufT
, und dann geschieht Folgendes:1
Punkte nach vorne zuT
1
Punkte nach hinten zuB
B
Punkte nach vorne zu1
T
Punkte nach hinten zu1
Diese Weise keine Position ist je "vergessen." Jetzt sagen Sie, dass
1
und2
sind zurück geschoben in verschiedenen threads auf der gleichen Liste. Es ist völlig plausibel, dass die Schritte (1) und (2) komplett für1
, dann2
wird komplett nach hinten geschoben, dann (1) muss fertig werden. Was passiert in diesem Fall? Wir haben die ListeB <-> 2 <-> T
, aber1
verweistB
undT
, so dass, wenn Ihre Zeiger sind angepasst, die Liste sieht wieB <-> 1 <-> T
, und das ist ein memory leak Sohn.Soweit Durchlaufen, ist es egal, ob Sie rückwärts oder vorwärts, du wirst immer bekommen, erhöht sich durch die Liste richtig. Dieses Verhalten hat nicht zu sein scheinen, garantiert durch die Norm, jedoch, so dass, wenn die codes dieses Verhalten hängt es zerbrechlich ist.
Was über die Größe???
Okay, also das ist wie Parallelität 101, eine alte Geschichte, wohl besser gesagt, viele Male, ich hoffe, dass es zumindest sehenswert, der code der Bibliothek. Das Größe Problem ist denke ich ein wenig interessanter, und ich habe sicherlich etwas von es herauszufinden.
Im Grunde, weil der Wert erhöht, ist nicht eine "lokale" variable, dessen Wert gelesen werden müssen in ein register Hinzugefügt wird, der Wert, dann wird dieser Wert gespeichert, zurück in die variable. Schauen wir uns einige der Baugruppe (meine assembly-Spiel ist schwach, bitte nicht nett sein, wenn Sie eine Korrektur). Betrachten Sie das folgende Programm:
Wenn ich mit objdump -D auf den Gegenstand, den ich bekommen:
4:
bewegt sich der Wert voni
ins registereax
.0x1
Hinzugefügteax
, danneax
ist wieder zurück ini
. So, was hat das zu tun mit Daten, die Rennen? Nehmen Sie einen anderen Blick auf die Funktion, die updates der Größe der Liste:Ist es vollkommen plausibel, dass die aktuelle Größe der Liste wird in ein register geladen, dann einen anderen thread, in Betrieb sind auf dieser Liste die Schritte auf unserem Betrieb. So, wir haben die alten Werte der Liste in einem register gespeichert, aber wir müssen sparen, der Staat und die transfer control verwenden, um jemand anderes. Vielleicht werden Sie erfolgreich hinzufügen eines Elements in die Liste und aktualisieren Sie die Größe, dann geben uns die Kontrolle zurück. Wir renovieren unser Land, sondern unser Staat ist nicht mehr gültig! Wir haben die alte Größe der Liste, die wir erhöhen, und deren Wert speichern wir wieder in Erinnerung, das vergessen, über den Betrieb dem anderen thread durchgeführt.
Eine Letzte Sache
Wie ich bereits erwähnt habe, ist die "Lokalität" der
i
ins Spiel kam, in dem oben genannten Programm. Wie wichtig das ist sehen Sie im folgenden:Hier kann es gesehen werden, dass kein Wert gespeichert ist, um ein register und kein register geschoben wird, um einige Variablen. Leider, soweit ich kann sagen, dies ist nicht irgendein netter trick, um zu vermeiden Probleme mit der Parallelität, wie mehrere threads Betriebssystem auf die gleiche variable wird unbedingt haben, darauf zu arbeiten durch memory access, und nicht nur durch die Register. Ich bin schnell raus aus meiner Liga hier, aber ich bin mir ziemlich sicher, dass das der Fall ist. Die nächste beste Sache ist, zu verwenden
atomic<int>
, aber dieses verdammte Ding ist schon zu lange.