POD mit CriticalSection ausstatten...
-
Hi!
Ich hätte gerne, dass meine class Settings; threadsafe wird, ich also ohne einen Gedanken an Synchronisation zu verschwenden settings->SetX(); aufrufen kann.
Ja, bei Set..() geht das ja noch, die Zuweisung einfach zwischen eine CS.
Aber wie soll ich es bei einem return machen? Muss ich da eine Kopie anlegen und diese zurückgeben? Oder ist das lesen einer Variable automatisch threadsafe? Und wenn ja, bis zu welcher Größe?Danke im Voraus!
MFG
-
Infos zu atomaren Operationen findest Du hier diskutiert.
http://groups.google.de/group/microsoft.public.de.vc/browse_frm/thread/542dc99ab5594b93/78fa137b0b1c59cb?hl=de&ie=UTF-8
-
Interessant, danke.
Wäre das dann so richtig benutzt?long Settings::GetValueX() { long temp; InterlockedExchange(&temp, this->valueX); return temp; }
-
Jetzt interessiert mich auch noch, wie es mit nicht-PODs funktioniert.
-
Ball2 schrieb:
Muss ich da eine Kopie anlegen und diese zurückgeben?
Ja.
Oder ist das lesen einer Variable automatisch threadsafe?
Nein.
Und wenn ja, bis zu welcher Größe?
Garnicht, siehe oben.
Jetzt interessiert mich auch noch, wie es mit nicht-PODs funktioniert.
Mit "nicht PODs" machst du es eben einfach mit einer Critical-Section. Beispiel:
std::string Settings::GetX() { EnterCriticalSection(&m_cs); std::string str = m_x; LeaveCriticalSection(&m_cs); return str; }
Die schönere Variante ist allerdings sich eine Scoped-Lock Klasse zu basteln:
class ScopedLock { public: explicit ScopedLock(CRITICAL_SECTION& cs) : m_cs(cs) { EnterCriticalSection(&m_cs); } ~ScopedLock() { LeaveCriticalSection(&m_cs); } private: ScopedLock(ScopedLock const&); // nicht kopierbar ScopedLock& operator = (ScopedLock const&); // nicht zuweisbar CRITICAL_SECTION& m_cs; }; std::string Settings::GetX() { ScopedLock lock(m_cs); return m_x; } // "lock" wird erst zerstört, nachdem bereits eine Kopie von "m_x" für den Returnwert angefertigt wurde. // d.h. das LeaveCriticalSection läuft auch erst nachher, daher ist dieser Code "sicher"
Und die noch schönere Variante ist wohl, gleich vorgefertige Klassen wie CCriticalSection & CSingleLock bzw. boost::mutex und boost::mutex::scoped_lock zu verwenden.
-
Danke erst einmal!
Leider bin ich jetzt total verwirrt, habe gerade etliche Beiträge zu "volatile" gelesen, weil die Interlocked...()-Funktionen nur volatile-Variablen annehmen. Und ich bin zu keinem Entschluss gekommen! JEDER sagt was anderes!Bitte bitte bitte, sagt mir die Wahrheit!
Warum sollte ich selbst mit synchronisation KEIN volatile benötigen? Der Compiler weiß nämlich angeblich nicht, was mit einer nicht-volatile Variable in einem anderen Thread passiert.
Somit müsste man doch IMMER volatile deklarieren, wenn in mehreren Threads auf einen primitiven Datentyp zugegriffen wird.Dann heißt es wieder, volatile braucht man nicht, warum auch immer.
Und wie ist es dann mit volatile und größeren Datentypen, Strukturen(membern?)? Alle member volatile deklarieren? Letztendlich ist ja jeder Datentyp aus primitiven Typen aufgebaut.
-
Vergiss einfach dass es das Keyword volatile gibt.
Der Compiler weiss nicht was mit einer Variable in einem anderen Thread passiert, richtig.Warum es trotzdem funktioniert ist ... nicht ganz einfach zu erklären, aber glaub mir, wenn du Locks ala CRITICAL_SECTION richtig verwendest, dann funktioniert es. Ohne volatile. Mal ganz davon abgesehen dass volatile auch nichtmal ausreichend wäre um irgendwas zu synchronisieren. (Auf speziellen Plattformen = CPU + Compiler Kombinationen funktionieren gewisse Dinge mit volatile zuverlässig, aber man kann nicht allgemein davon ausgehen, da der Standard es nicht vorschreibt)
Dass jeder was anderes sagt, und du so viele widersprüchliche Informationen zu dem Thema findest, liegt einfach daran, dass das Thema nicht ganz einfach ist, und es so viele Leute nicht verstanden haben (aber leider trotzdem glauben dass sie es verstanden haben).
Der Verweis von Martin auf Interlocked-Operationen ist auch nicht verkehrt, so war meine Antwort nicht zu verstehen. Meine Antwort war hauptsächlich auf die Frage "wie es mit nicht-PODs funktioniert" ausgelegt, bzw. auch auf PODs die aus mehr als nur einem Wert bestehen. Und dazu braucht man nunmal Locks.
Bzw. finde ich, gerade für Leute die sich noch nicht so gut mit dem Thema auskennen, die Verwendung von Locks auch einfacher, als die (korrekte) Verwendung von Interlocked-Funktionen.----
Was das volatile angeht: Die Interlocked-Funktionen erwarten sich zwar volatile Zeiger, aber die Variablen die man verwendet müssen deswegen nicht volatile sein. Dass die Interlocked-Funktionen volatile Zeiger nehmen, ermöglicht einem bloss, die Funktionen mit volatile Variablen zu verwenden (volatile ist in dieser Hinsicht gleich wie const -- eine Variable muss ja auch nicht const sein, nur weil eine Funktion z.B: einen "const int*" haben möchte).
Aber wie gesagt: vergiss einfach dass es das Keyword volatile gibt. Wenn du ganz normal mit Locks arbeitest, wie es "alle anderen" auch machen, dann brauchst du volatile nicht.
-
volatile verhindert manche optimierungen.
so macht jeder einigermaßen sinnige compiler ausx=5; while(!kbhit()){ result+=x*4; } return result;
ein
x=5; while(!kbhit()){ result+=20; } return result;
oder?
mit volatile x wäre das nicht passiert. da würde der compiler damit rechnen, daß ein anderer thread das x zwischenduch verändert.
so gesehen müßten theoretisch die ganzen threadkomminikationssachen volatile sein. in der praxis reicht es aber völlig aus, daß man den kram an stellen schreibt, wo kein vernünftiger compiler mit der variablen etwas optimieren will, oder interlocked-funktionen nimmt, die das optimieren selber schon verhindern.
volatile wegzulassen ist also eine sünde.
kaum einer kennt volatile überhaupt und die welt dreht sich noch.mit keinem MSVC ist mir volatile-weglass-technisch je was passiert. nur mit borland c++ und gcc.
-
hustbaer schrieb:
Warum es trotzdem funktioniert ist ... nicht ganz einfach zu erklären, ...
Das würde ich sehr gerne wissen...
hustbaer schrieb:
Vergiss einfach dass es das Keyword volatile gibt.
volkard schrieb:
volatile wegzulassen ist also eine sünde.
Was nun...
-
Das würde ich sehr gerne wissen...
Also ich versuche es einfach zu erklären.
Erstmal vorweg die "offizielle" Variante: du brauchst kein volatile, weil die verwendete API (WinAPI, PTHREADS, ...) dir garantiert, dass es ohne volatile funktioniert.Nun zum warum, bzw. zu dem wie es wirklich funktioniert, was wie gesagt etwas schwieriger zu erklären ist.
Überleg' dir mal auf welche Daten (Objekte/Variablen) ein Thread überhaupt zugreifen kann. Das wären:
* lokale Objekte/Variablen
* globale Objekte/Variablen
* Objekte/Variablen auf die der Thread einen Zeiger bekommen hat (als Parameter der Thread-Funktion)
Sowie natürlich alle weiteren Objekte/Variablen, die, ausgehend von der Liste oben, über Zeiger erreichbar sind.Lokale Variablen sind erstmal egal - solange ein anderer Thread nicht irgendwie deren Adresse kennt, kann er nicht darauf zugreifen. Und solange nur ein Thread zugreift gibt es kein Problem. Wir können diese also streichen. Sollte ein anderer Thread die Adresse von lokalen Variablen kennen, so muss er diese auf einem der anderen beiden Wege mitgeteilt bekommen, d.h. über globale Variablen oder den Thread-Parameter. Somit fallen diese unter die Kategorie "über Zeiger erreichbar".
Nun sehen wir uns so einen CreateThread Aufruf an. Dieser bekommt als Parameter normalerweise einen Funktionszeiger, und einen "void*" der auf irgendwas zeigen kann. Das OS übergibt diesen void* dann an die Thread-Funktion. Das weiss der Compiler aber nicht, da der Compiler keine Kenntnis darüber hat, was Betriebssystem-Funktionen machen. Bzw. auch nicht was Funktionen in fertigen vorcompilierten DLLs machen. Er sieht deren Code ja nicht (bei DLLs nichtmal den fertigen Maschinencode, da man zum Linken nur die Import-Lib braucht, und die enthält den Maschinencode ja nicht).
D.h. der Compiler hat also keine Ahnung was z.B. CreateThread, EnterCriticalSection oder LeaveCriticalSection machen.
Nun stell dir vor diese Funktionen wären folgendermassen implementiert (pseudocode, ganze ohne Threads):
void CreateThread(Funktion* f, void* p) { globale_zeigerliste.push_back(p); // wir merken uns die adresse "p" } void EnterCriticalSection(CRITICAL_SECTION* cs) { für alle p in globale_zeigerliste { memset(p, 0, 4); // wir setzen die ersten 4 byte auf die "p" zeigt auf 0 } } void LeaveCriticalSection(CRITICAL_SECTION* cs) { für alle p in globale_zeigerliste { int* i = static_cast<int*>(p); printf("%d\n", *i); // wir geben den wert auf den "p" zeigt auf der Konsole aus } }
Zusammengefasst: CreateThread speicher alle übergebenen Adressen in einer globalen Liste, und EnterCriticalSection bzw. LeaveCriticalSection greifen auf diese Liste zu, um die referenzierten Variablen auszulesen.
Der Compiler kann nicht wissen dass die Funktionen nicht so implementiert sind, muss also Code erzeugen, der mit dieser Implementierung korrekt funktioniert.Das bedeutet dass er z.B. vor jedem Aufruf von EnterCriticalSection oder LeaveCriticalSection sämtliche Variablen, deren Adressen irgendwann mal an CreateThread übergeben wurden, in den Speicher zurückschreiben muss (da Enter-/LeaveCriticalSection diese Variablen ja auslesen könnte), und nach dem Aufruf die Variablen neu einlesen muss wenn der Wert wieder benötigt wird (da Enter-/LeaveCriticalSection diese Variablen ja verändert haben könnte).
Und das alles noch ganz ohne Threads.
Für globale Variablen gilt im Prinzip dasselbe.D.h.: der Compiler muss bei sämtlichen Variablen, die evtl. von einem anderen Thread verwendet werden könnten, davon ausgehen, dass die Funktionen CreateThread/Enter-/LeaveCriticalSection diese Variablen selbst lesen und/oder verändern.
Sämtliche Dinge die der Compiler also durch Optimierungen "anrichten" könnte (Variablen in Registern halten etc.) sind dadurch also schonmal ausgeschlossen. Bleiben noch diverse Dinge wie Reordering und Cache-Coherency was die CPU angeht. Und um diese Dinge kümmern sich kurz gesagt die entsprechenden Funktionen. Also Enter-/LeaveCriticalSection enthält die nötigen Assembler-Befehle, die sicherstellen, dass die CPU auch nicht dreinpfuschen kann.
----
Das ist jetzt leider etwas plump formuliert, und vielleicht nicht sehr gut verständlich. Um es besser zu machen müsste ich mir allerdings wesentlich mehr Zeit nehmen, und das is mir echt zuviel Action für einen Beitrag in einem Forum, der bald "verschwindet", und von nur wenigen Leuten gelesen wird.
Vielleicht verstehst du es ja trotzdem
-
Dankesehr
Leider nicht ganz verstanden. Wenn man an den void*-Parameter von CreateThread() übergibt..OK. Aber bei globalen Variablen hat der Compiler doch keine Ahnung ob eine Funktion durch einen eigenen Thread ausgeführt wird?
Oder sind es wirklich einfach nur die Systemcalls, welche den ganzen Krampf erledigen, dem Compiler bescheid geben?zB. sowas, funktioniert jedenfalls, es wird anscheinend nichts wegoptimiert:
CRITICAL_SECTION cs; int var = 1; unsigned int __stdcall Thread2(void* param) { Sleep(3000); EnterCriticalSection(&cs); var = 0; LeaveCriticalSection(&cs); return 0; } int main() { InitializeCriticalSection(&cs); _beginthreadex(0, 0, Thread2, 0, 0, 0); while(true) { EnterCriticalSection(&cs); if(!var) break; LeaveCriticalSection(&cs); } DeleteCriticalSection(&cs); }
Und gibt es wirklich ausnahmslos keine Fallstricke?
-
Der MS Compiler setzt sogenannte Synchronisations Punkte.
Ich weiß nicht ganau ob diese der richtige Begriff ist, Du findest ihn in der C++ Compiler Bschreibung in der MSDN.Durch den Aufruf der externen Funktionen wird der Copiler keine Annahme darüber machen, dass die Variable unverändert bleibt.
volatile wäre hier korrekt, aber der MS Compiler optimiert hier nicht.
-
Puh, echt kein einfaches Thema.
Ich wäre gern 100 Jahre später geboren
-
Ball2 schrieb:
Aber bei globalen Variablen hat der Compiler doch keine Ahnung ob eine Funktion durch einen eigenen Thread ausgeführt wird?
Oder sind es wirklich einfach nur die Systemcalls, welche den ganzen Krampf erledigen, dem Compiler bescheid geben?Es geht ja genau darum, dass der Compiler nichtmehr wissen kann ob eine Funktion ala CreateThread oder EnterCriticalSection bestimmte Variablen ändern kann, und daher gezwungen ist davon auszugehen, dass sie geändert werden. Der Compiler ist ja sozusagen nicht nur bei Variablen "vorsichtig" von denen er weiss dass sie geändert werden, sondern bei allen die eventuell geändert werden könnten.
Und dein Beispielcode funktioniert "garantiert", ja. Bis auf dass du eine CRITICAL_SECTION zuerst mit LeaveCriticalSection "freigeben" solltest, bevor du sie mit DeleteCriticalSection "löscht". Was du in deinem Beispielcode nicht machst.
Und ich bleibe dabei: volatile ist hier nicht nötig.
----
Martin Richter schrieb:
Durch den Aufruf der externen Funktionen wird der Copiler keine Annahme darüber machen, dass die Variable unverändert bleibt.
volatile wäre hier korrekt, aber der MS Compiler optimiert hier nicht.Genau. Der Compiler optimiert nicht, weil er nicht optimieren kann.
Leider ist in der MSDN nicht klar dokumentiert, dass EnterCriticalSection eine acquire barrier darstellt, und LeaveCriticalSection eine release barrier (zumindest hätte ich keine entsprechende Stelle gefunden). Da allerdings die InterlockedXxx Funktionen (die wesentlich mehr "low level" sind) dokumentierterweise barriers darstellen, denke ich kann man davon ausgehen, dass auch Enter-/LeaveCriticalSection barriers sind. Zumindest verlassen sich Heerscharen von Windows-Programmierern genau darauf. Und auf Wintel Systemen ist es auch so.
Die PTHREADS API z.B. garantiert es explizit. Das "locken" einer Mutex hat mindestens acquire semantics, das "releasen" einer Mutex hat mindestens release semantics. Damit ist volatile vollkommen überflüssig.
-
OK!
Damit sind die Fragen nun geklärt, vielen Dank nochmals!(Achja, in meinem Fall würde doch sowieso immer LeaveCriticalSection() vor Delete..() aufgerufen werden?!)
-
@hustbaer:
Ich bin der Meinung, dass man volatile öfters nutzen sollte als es der "normale" Programmierer tut. Alleine schon auch aus dokumentationstechnischen Gründen.
Alleine das Beispiel für volatile in der MSDN zeigt dies. Denn es darf kein Reodering hier geben.
http://msdn.microsoft.com/en-us/library/12a04hfd(VS.80).aspxLeider finde ich die Doku nicht mehr über die Marken zwischen denen der C++ Compiler Annahmen über den Wert von Variablen machen kann.
Grundsätzlich kann man aber davon ausgehen, dass ein Aufruf einer externen Function, die nicht in dem aktuellen Modul liegt als Punkt anzusehen ist, ab dem der Microsoft Compiler keine Annahmen mehr über den Wert einer externen Variable macht.
Dies schließt auch Variablen auf dem Stack ein, deren Adresse ermittelt wurde.
-
volkard schrieb:
volatile verhindert manche optimierungen.
so macht jeder einigermaßen sinnige compiler ausx=5; while(!kbhit()){ result+=x*4; } return result;
ein
x=5; while(!kbhit()){ result+=20; } return result;
oder?
mit volatile x wäre das nicht passiert. da würde der compiler damit rechnen, daß ein anderer thread das x zwischenduch verändert.fast richtig. der compiler berücksichtigt zwar kein threading, aber 'volatile' sorgt dafür, dass x auch tatsächlich existiert und exakt an der stelle benutzt wird, an der du's im code geschrieben hast. aus einer funktion:
int f() { static int x; lock(); x++; unlock(); return x; }
dürfte der compiler, ohne volatile vor dem x, das machen:
int f() { static int x; lock(); unlock(); return ++x; }
weil lock() und unlock() keinen verweis auf 'x' haben, darf er dies tun. dass lock()/unlock() die variable vor dem gleichzeitigen zugriff durch mehrere threads schützen soll, ist dem compiler unbekannt.