Probleme und Fragen zu Thread-Synchronisation ;)
-
Guten Tag zusammen
,Ich habe mich zwar schon intentiv über dieses Thema eingelesen, habe aber dennoch ein paar konkrete und auch generelle Fragen zur Thread-Synchronisation:
1. Wann sollte man _welche_ 'Synchronisations-Methode' anwenden:
a) Kritischer Abschnitt (-> InitializeCriticalSection, ...) ?
b) Ereignisobjekt (-> CreateEvent, ...) ?
c) Mutext-Objekte (-> CreateMutex, ...) ?2. Wenn ich eine Klasse habe, 'CDataMgm' (für Data-Management
), und diese Klasse beispielsweise einen std::vector enthält, indem sind eine Menge Daten abgelegt (Das Vektorobjekt selbst enthält aber nur Zeiger auf Objekte, der Vektor ist also ein Array aus Zeigern). Im einem Sekundär-Thread wird diese Klasse nun über die entsprechenden Zugriffsmethoden mit 'frischen Daten' befüllt (=aktualisiert). In meinem Hauptthread sollen diese Daten in einer Liste (ListBox oder ListView) angezeigt werden. Wenn ich jetzt eine Zugriffsmethode der Klasse im Sekundär-Thread aufrufe (die sich seinerseits ja wiederrum einer Methode der Klasse std::vector bedient), wie, bzw. wo und vor allem womit (siehe Frage 1) muss/sollte ich diesen Zugriff jetzt synchronisieren (Ich hoffe es ist klar geworden, was gemeint ist :xmas2: )?Eine weitere Unklarheit wäre folgende: Mal angenommen diese oben beschriebene Klasse hat eine Methode die weitere Methoden aufruft, also so etwas:
void CDataMgm::CleanUp(void) { if(_hIrgWas != NULL) { DeleteObject(_hIrgWas); _hIrgWas = NULL; } for(int i = 0; i < _iInternalCounter; i++) delete _pObjArray[i]; DeletePrimData(); // << Aufruf einer 'Untermethode' if(_hNochWas != NULL) { DeleteObject(_hNochWas ); _hNochWas = NULL; } }Mal angenommen die Methode 'DeletePrimData()' ist als public deklariert. So könnte man sie also auch aus dem Sekundär- bzw. Hauptthread heraus aufrufen. Das heißt ja, das man sie synchronisieren (also absichern) muss, oder ? Da die Methode 'CleanUp' aber auch gesichert werden muss, stellt sich für mich die Frage, wie man nun (beispielsweise) kritische Abschnitte setzen müsste... . Würde man den obrigen Methodenaufruf 'DeletePrimData()' einfach mit einschließen ? Oder darf man das gar nicht ?
Mal angenommen die Methode ist so definiert:
void CDataMgm::DeletePrimData(void) { // Vorher noch 'ne Benachrichtigung ... (Beispiel) for(UINT i = 0; i < _vecData.size(); i++) // _vecData ist ein std::vector-Objekt if(_vecData[i]->fSet) SendMessage(_hWnd, WM_IRGWAS, 0, 1); for(UINT i = 0; i < _vecData.size(); i++) delete _vecData[i]; // Beispiel-Definition von _vecData: std::vector<CObject*> _vecData; _vecData.resize(0); _fObjectsDeleted = true; }Ich hoffe, dass ich auch nicht mit zu viel Text bombardiert habe und danke schonmal jedem, der sich das durch liest bzw. hier Klarheit schaffen kann
.Ich hätte das jetzt einfach so synchronisiert (glaube aber das ist falsch oder nicht sinnvoll
):/* _cs wurde vorher im Konstruktor mit InitializeCriticalSection korrekt initailisiert. */ // CleanUp: void CDataMgm::CleanUp(void) { EnterCriticalSection(&_cs); if(_hIrgWas != NULL) { DeleteObject(_hIrgWas); _hIrgWas = NULL; } for(int i = 0; i < _iInternalCounter; i++) delete _pObjArray[i]; DeletePrimData(); // << Aufruf einer 'Untermethode' if(_hNochWas != NULL) { DeleteObject(_hNochWas ); _hNochWas = NULL; } LeaveCriticalSection(&_cs); } // DeletePrimData: void CDataMgm::DeletePrimData(void) { EnterCriticalSection(&_cs); // Vorher noch 'ne Benachrichtigung ... (Beispiel) for(UINT i = 0; i < _vecData.size(); i++) // _vecData ist ein std::vector-Objekt if(_vecData[i]->fSet) SendMessage(_hWnd, WM_IRGWAS, 0, 1); for(UINT i = 0; i < _vecData.size(); i++) delete _vecData[i]; // Beispiel-Definition von _vecData: std::vector<CObject*> _vecData; _vecData.resize(0); _fObjectsDeleted = true; LeaveCriticalSection(&_cs); }MfG & nachträglich Frohe Weihnachten
BitteUmHilfe :xmas1: !
-
- ja das kann man so machen - kommt natürlich drauf an was im Handler für "WM_IRGWAS" passiert.
- gewöhn dir ab alles krampfhaft zu übersetzen, eine critical section ist eben eine critical section, und kein "kritischer abschnitt" (*schauder*). selbiges gilt für "event".
- man verwendet die diversen synchronization primitives so wie man sie eben braucht - und in fällen wo man entweder eine mutex oder eine critical section verwenden kann nimmt man letzters weils (ordentlich viel) schneller ist.
-
Puh, soviel geschrieben und sonne kleine Antwort ... Naja danke erstmal @hustbear! ...Vllt kommt ja noch n erklärender Text
.hustbaer schrieb:
- ja das kann man so machen
Aber blockiert sich mein Programm denn damit nicht selbst ?
Siehe hier:// CleanUp: void CDataMgm::CleanUp(void) { EnterCriticalSection(&_cs); // Code... DeletePrimData(); // << Aufruf einer 'Untermethode' // Code... LeaveCriticalSection(&_cs); } // DeletePrimData: void CDataMgm::DeletePrimData(void) { /* Die critical section ist doch bereits 'gelocked' (eben in 'CleanUp'), also kann sie doch nicht nochmal 'geentered' werden, oder ? */ EnterCriticalSection(&_cs); // Code... LeaveCriticalSection(&_cs); }hustbaer schrieb:
kommt natürlich drauf an was im Handler für "WM_IRGWAS" passiert.
Was dürfte denn da nicht drin stehen (damit man also ein Crash verhindert)?
-
Wenn ich meinen Hintern ganz lange auf dem Parkett reibe, dann tut das weh

-
Da du ja weißt, dass in DeletePrimData auch der kritische Abschnitt (:D) genutzt wird, würde ich die Section vor dem Aufruf von DeletePrimData verlassen und danach wieder eintreten.
Das gibt natürlich ein Problem, wenn die Daten nach dem LeaveCriticalSection in CleanUp und vor dem Eintreten in DeletePrimData nicht von einem anderen Thread benutzt werden dürfen. In dem Fall würde ich den Code von DeletePrimData direkt in CleanUp einbauen.
Ich hoffe das war verständlich, ist etwas verwirrend zu lesen... Naja, Code:void CDataMgm::CleanUp(void) { EnterCriticalSection(&_cs); ... LeaveCriticalSection(&_cs); DeletePrimData(); // << Aufruf einer 'Untermethode' EnterCriticalSection(&_cs); ... LeaveCriticalSection(&_cs); } void CDataMgm::DeletePrimData(void) { EnterCriticalSection(&_cs); ... LeaveCriticalSection(&_cs); }So känntest du das dann auch generell machen, z.B. auch wenn du deine Nachricht sendest.
Ansonsten hab ich auch nicht soviel Ahnung von der Materie, ich selber benutze ausschließlich Criticial Sections und bin damit auch immer gut ausgekommen.
Achso, zu Events: Die würde ich mit Critical Sections und Mutexen nicht in eine Kiste werfen, die braucht man ja eher für andere Zwecke (meistens^^).
Z.B. sind Events sehr nützlich um quasi eine boolean-Variable zwischen Threads zu haben, auf deren Statusänderung man auch noch warten kann.
Events brauche ich eigentlich nur für 3 Sachen:1. Für Win-Krams, z.B. wenn ich darauf warte, dass an einem Socket von mir was passiert. Da hab ich einfach das Event und warte mit WaitForSingleObject/MultipleObjects.
2. Um einem Thread klarzumachen, dass er beendet werden soll. Jeder Thread den ich erstelle bekommt bei mir vom Haupt-Thread einen Event als Parameter mitgeschickt. Wenn der Event gesetzt wird, soll sich der Thread beenden. Das kann der mit WaitForSingleObject(hevAbort,0)!=WAIT_TIMEOUT prüfen. Und der Hauptthread kann mit SetEvent(hevAbort); if ( WAIT_TIMEOUT == WaitForSingleObject(hevAbort,1000); dem Neben-Thread eine Deadline zum Beenden geben. Die Methode ist aber umstritten, Jochen z.B. bevorzugt ein INFINITE-Warten auf Threads.
3. Als Bool-Variable zwischen Threads. Kann man auch als Ersatz für ne Critical Section nehmen, dann halt mit Timeout-Warten.
EDIT: Hab grad was in der MSDN gefunden:
MS schrieb:
After a thread has ownership of a critical section, it can make additional calls to EnterCriticalSection without blocking its execution. This prevents a thread from deadlocking itself while waiting for a critical section that it already owns.
Mal wieder was gelernt... Anscheinend kann dein Thread sich mit Critical Sections nicht selber blockieren. Also müsste es doch so gehen, wie du es hast... (gut dass ich geantwortet hab
)
-
Die beiden "native" Locks die es in Windows gibt (CRITICAL_SECTION und MUTEX) sind beide reentrant, also der gleiche Thread kann die ohne weiteres nochmal locken, muss sie dann aber auch nochmal freigeben. Von daher ist der 2. Aufruf von EnterCriticalSection und LeaveCriticalSection in DeletePrimData kein Problem.
Mittels einer "mutex" (womit ich jetzt das "mutex" Konzept meine, welches sowohl von einer CRITICAL_SECTION als auch von einer MUTEX erfüllt wird) synchronisiert man normalerweise irgendwelche "Transaktionen" auf Daten, die nicht unterbrochen werden dürfen. z.B. eine Variable lesen, den Wert um 1 erhöhen, und wieder zurückschreiben (read-modify-write), bzw. natürlich meistens längere Vorgänge.
Die "mutex" jetzt vor dem Aufruf einer Unterfunktion wieder "unlocken" will man daher meistens nicht, da an der Stelle ein anderer Thread dazwischenfunken könnte -- sonst müsste man an der Stelle auch dafür sorgen dass sämtliche Invarianten erfüllt sind, und dürfte sich danach auch auf keine Werte mehr verlassen die man vor dem "unlocken" der "mutex" ermittelt hat.
Es kann aber auch unter Windows bzw. allgemeiner mit reentranten Locks Sinn machen mehrfaches "locken" zu vermeiden, da das "lockes" und "unlocken" nämlich Zeit kostet, und nichtmal ganz wenig. Dazu macht man sich dann meist "NoLock" Funktionen, wo der eigentliche Code drinnen steht, und zu jeder "NoLock" Funktion eine weitere mit gleichem Namen & Parametern, welche sich bloss den Lock holt, und dann die "NoLock" Funktion aufruft. z.B. so.:
void CDataMgm::DeletePrimData(void) { EnterCriticalSection(&_cs); DeletePrimDataNoLock(); LeaveCriticalSection(&_cs); } void CDataMgm::DeletePrimDataNoLock(void) { // Vorher noch 'ne Benachrichtigung ... (Beispiel) for(UINT i = 0; i < _vecData.size(); i++) // _vecData ist ein std::vector-Objekt if(_vecData[i]->fSet) SendMessage(_hWnd, WM_IRGWAS, 0, 1); for(UINT i = 0; i < _vecData.size(); i++) delete _vecData[i]; // Beispiel-Definition von _vecData: std::vector<CObject*> _vecData; _vecData.resize(0); _fObjectsDeleted = true; }Dann kannst du von CleanUp aus einfach DeletePrimDataNoLock aufrufen da die "mutex" (in diesem Fall: CRITICAL_SECTION) ja schon gelockt ist - geht dann etwas flotter. In diesem speziellen Beispiel wahrscheinlich vollkommen egal, aber bei Performance-kritischem Code kann das schon einen Unterschied machen.
hustbaer schrieb:
kommt natürlich drauf an was im Handler für "WM_IRGWAS" passiert.
Was dürfte denn da nicht drin stehen (damit man also ein Crash verhindert)?
z.B. dürfte in dem Handler keine Memberfunktion eines anderen CDataMgm Objektes aufgerufen werden, sonst könnte das u.U. zu einem Deadlock führen.
Ist etwas schwer zu erklären, aber wenn du z.B. 2 Objekte hast, A und B, und dann folgendes passiert:// thread 1 A.foo(); // in A.foo(): Lock(this->mutex); // hier wird A.mutex gelockt SendMessage(blubb); // im Handler für blubb: this->other->bar(); // wobei "other" auf B zeigt // in B.bar(): Lock(this->mutex); // hier wird B.mutex gelockt Irgendwas(); Unlock(this->mutex); Unlock(this->mutex); // thread 2 B.foo(); // in B.foo(): Lock(this->mutex); // hier wird B.mutex gelockt SendMessage(blubb); // im Handler für blubb: this->other->bar(); // wobei "other" auf A zeigt // in A.bar(): Lock(this->mutex); // hier wird A.mutex gelockt Irgendwas(); Unlock(this->mutex); Unlock(this->mutex);Wenn nun beide threads "gleichzeitig" bis zu dem SendMessage laufen, dann hast du einen sog. "deadlock": thread 1 hat A.mutex gelockt und will B.mutex locken, thread 2 hat B.mutex gelockt und will A.mutex locken. Keiner der beiden Threads wird "seine" mutex wieder hergeben -> beide werden ewig warten.
Diese Fälle kann man natürlich alle irgendwie verhindern, aber man kann auch allzuleicht drüberstolpern. Klassisches Beispiel:class Foo { public: // "böse": Foo& operator =(Foo const& other) { Lock(m_mutex); Lock(other.m_mutex); m_str = other.m_str; Unlock(other.m_mutex); Unlock(m_mutex); // Problem: was wenn gleichzeitig "a = b;" und "b = a;" ausgeführt wird? } // besser: Foo& operator =(Foo const& other) { Mutex* m1 = &m_mutex; Mutex* m2 = &other.m_mutex; if(this > &other) std::swap(m1, m2); Lock(*m1); Lock(*m2); m_str = other.m_str; Unlock(*m2); Unlock(*m1); } private: Mutex mutable m_mutex; std::string m_str; };Gottseidank stolpert man aber relativ selten über solche Probleme, es sei denn man will wie hier gezeigt eben eine Klasse threadsafe machen, deren Objekte irgendwie interagieren können. Genau das muss man aber normalerweise selten, da man normalerweise nur dort "synchronisiert" wo man eben mehrere Threads zu synchronisieren hat, d.h. der Programmteil welcher die Threads anlegt bzw. mit ihnen arbeitet kümmert sich um die Synchronisierung, nicht alle Klassen intern.
-
Sry das das so lange gedauert hat mit der Antwort...
Ich danke euch vielmals, ihr habt mir die Augen geöffnet
.Das wars, danke danke!!
Hab noch eine *kleine* Frage:
Wofür brauche ich InterlockedIncrement ?
-
BitteUmHilfe schrieb:
Hab noch eine *kleine* Frage:
Wofür brauche ich InterlockedIncrement?damit kann man die eingebauten locks des prozessors benutzen. damit kannste z.b. einen 'atomaren counter' basteln. der ist aber nur atomar, solange andere threads auch mit 'InterlockedXXX'-funktionen darauf zugreifen. weil solche locks per hardware gemacht werden, sind die natürlich um zehnerpotenzen performanter als alles was das betriebssystem anbietet...
:xmas2:
-
ten schrieb:
weil solche locks per hardware gemacht werden
WOW! Sperrt dann der Prozessor den Speicherbereich oder wie? Und kann die Hardware noch mehr so abgefahrene Sachen? Dachte, die Sachen würden einfach nur vor sich hinrechnen...
-
Sperrt dann der Prozessor den Speicherbereich oder wie?
Naja, die vorhandenen Cores (egal ob auf dem gleiche Chip oder verschiedenen Chips) kommunizieren über den FSB. Bei einem "interlocked" Befehlt teilt dann im Prinzip ein Core den anderen mit "ich will bitte jetzt was 'interlocked' auf dieser bestimmten Cache-Line machen", und wartet bis die anderen ihr "OK" geben. Dann führt er den Befehl aus, und teilt den anderen Cores dann mit dass er nu fertig ist. Das ganze dauert im übrigen ziemlich lange verglichen mit einem "normalen" Befehl, man sollte "interlocked" Befehle daher so selten wie möglich einsetzen, aber natürlich so oft wie nötig.
Und kann die Hardware noch mehr so abgefahrene Sachen?
"Die Hardware" kann leider viel zu wenig, d.h. zumindest normale x86 Boxen.
z.B. gibt es keine möglichkeit direkt mehr als einen Befehl in Folge "interlocked" auszuführen - was man aber oft gut brauchen könnte. Genauso wäre es für gewisse Dinge hilfreich wenn man eine Möglichkeit hätte eine Speicherzelle zu "taggen", um dann ein paar Befehle später sagen zu können "schreib xyz in die getaggte Speicherzelle, aber bloss wenn seit setzen des Tag kein anderer Core da reingeschrieben hat".----
weil solche locks per hardware gemacht werden, sind die natürlich um zehnerpotenzen performanter als alles was das betriebssystem anbietet...
Auf Windows (Pentium 4 mit HT) braucht das:
unsigned long g_l = 0; // ... void foo() { bool ok = false; ::EnterCriticalSection(&g_cs); if(g_l == 0) { g_l = 1; ok = true; } ::LeaveCriticalSection(&g_cs); }bloss etwa 3x solange wie das:
unsigned long volatile g_l = 0; // ... void foo() { bool ok = ::InterlockedCompareExchange(&g_l, 1, 0) == 0; }Vorausgesetzt natürlich die CRITICAL_SECTION ist "frei".
Von Zehnerpotenzen kann man also nicht ganz reden
Eine MUTEX ist allerdings wirklich um Zehnerpotenzen langsamer als ein interlocked Befehl oder auch einen CRITICAL_SECTION, weswegen man ja auch immer ne CRITICAL_SECTION verwendet wenn man nicht unbedingt die erweiterten "Features" einer MUTEX braucht.
Unter *NIX siehts ähnlich aus sofern "usermode spin-locks" verfügbar sind - die meisten PTHREADS mutex Implementierungen sind aber nen schönes Stück langsamer als eine CRITICAL_SECTION oder ein "usermode spin-lock".