JLI Spieleprogrammierung Foren-Übersicht JLI Spieleprogrammierung

 
 FAQFAQ   SuchenSuchen   MitgliederlisteMitgliederliste   BenutzergruppenBenutzergruppen 
 medals.php?sid=a1c05123f072d16284463567ee2385baMedaillen   RegistrierenRegistrieren   ProfilProfil   Einloggen, um private Nachrichten zu lesenEinloggen, um private Nachrichten zu lesen   LoginLogin 

Thread Programmierung: Synchronisation

 
Neues Thema eröffnen   Neue Antwort erstellen    JLI Spieleprogrammierung Foren-Übersicht -> Tutorials
Vorheriges Thema anzeigen :: Nächstes Thema anzeigen  
Autor Nachricht
David
Super JLI'ler


Alter: 39
Anmeldedatum: 13.10.2005
Beiträge: 315

Medaillen: Keine

BeitragVerfasst am: 24.11.2006, 13:54    Titel: Thread Programmierung: Synchronisation Antworten mit Zitat

Hi!

Hier noch eine kleine Ergänzung zum Thema Multithreading und Thread Synchronisierung des Tutorials von Dragon: http://www.jliforum.de/board/viewtopic.php?t=4795
(sorry das ich kritische Bereiche nochmal angerissen hab', kam mir aber ohne diese nicht Vollständig vor! Smile)

2.0 Thread Synchronization

2.1 Wozu Threads synchronisieren?
Synchronisieren von Threads ist ein ganz essentieller Bestandteil der „multithreaded“ Programmierung. Es macht nicht immer Sinn Threads wild Durcheinander ablaufen zu lassen, manchmal geht es um sensible Daten, die für mehr als einen Thread zur Verfügung stehen, welche aber bei einer bestimmten Bearbeitungsvorgang nicht von mehreren Threads auf einmal bearbeitet werden dürfen.
Man stelle sich z.B. vor es findet ein virtueller Geldtransfer auf ein Bankkonto statt. Zeitgleich startet der Prozess einen weiteren Transfer in einem weiteren Thread. Sieht man sich folgenden Pseudocode an wird evtl einiges klar:
CPP:
Funktion Geldtransfer( Betrag )         (1)
   Kontostand = leseAltenKontostand()   (2)
   Kontostand += Betrag            (3)
   kontostandSetzen( Kontostand )      (4)
Ende der Funktion                  (5)


Dies ist natürlich nur eine stark vereinfachte Version eines solchen Vorgangs, allerdings sieht man hier recht gut das Problem. Wenn der erste Thread nach Zeile 2 abgebrochen wird und Thread Nummer zwei aktiv wird so hat Thread Nummer eins, bei seinem Wiedereintritt, immer noch den alten Kontostand. Wenn also beim ersten Transfer 5.000 Euro und beim zweiten Transfer 7.000 Euro übertragen werden und der Kontostand zu Beginn 10.000 Euro beträgt, so gehen einfach mal 7.000 Euro verloren, und sowas ist nie wünschenswert, oder?
Bei Multithreaded Applikationen besteht diese Gefahr aber nun mal und deswegen muss für so kritische Bereiche Abhilfe geschaffen werden. Der Vorgang muss also abgeschlossen werden bevor ein anderer Thread (oder Prozess) auf den selben Vorgang zugreifen kann.
Die Frage ist allerdings, wie soll man einen Thread davon überzeugen zu warten bis die sensiblen Daten wieder freigegeben wurden? Und genau für dieses Problem gibt es sogar eine Vielzahl von möglichen Lösungen.

2.2 Kritische Bereiche
Eine Lösung zu o.g. Problem bieten die „Critical Sections“ welche in der WinAPI verfügbar sind. Die Idee ist folgende:
CPP:
Function Foobar()
   Eintritt in den kritischen Bereich
   
   Verarbeitung von Daten, etc...

   Verlassen des kritischen Bereichs
Ende der Funktion


Sobald ein Thread versucht in einen kritischen Bereich zu gelangen muss er solange warten bis dieser Bereich freigegeben wird. Schafft es der Thread schlussendlich in den kritischen Bereich zu kommen so sperrt er diesen für alle anderen Threads und gibt in erst wieder frei wenn er seinem Verarbeitungsprozess vollständig abgeschlossen hat.
Die WinAPI bietet für solche kritischen Bereiche einige Funktionen.

CPP:
InitializeCriticalSection(
   LPCRITICAL_SECTION lpCriticalSection
);


Die Funktion InitializeCriticalSection dient zum erstellen eines kritischen Bereichs. Als einzigen Parameter wird ein Zeiger auf die Struktur CRITICAL_SECTION erwartet. Diese hat folgendes Aussehen.

CPP:
typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;
    HANDLE LockSemaphore;
    DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;


Das Initialisieren eines kritischen Bereichs ist der erste Schritt der zu tun ist um CriticalSections zu verwenden. Das Gegenstück, also das Löschen von kritischen Bereichen wird von folgender Funktion gemanaged.

CPP:
void DeleteCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);


Auch hier wird ein Zeiger auf ein Objekt vom Typ CRITICAL_SECTION erwartet. Sollte ein kritischer Bereich gelöscht werden, der zu dem Zeitpunkt nicht wieder freigegeben wurde, so ist das Verhalten der „wartenden“ Threads nicht definiert.
Die nächsten zwei Funktionen dienen zu absperren und freigeben von kritischen Bereichen, die Deklaration sieht aus wie folgt.

CPP:
void EnterCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);

void LeaveCriticalSection(
  LPCRITICAL_SECTION lpCriticalSection
);


Beide Funktionen erwarten die Übergabe eines Zeigers auf ein initialisiertes Objekt vom Typ CRITICAL_SECTION. Die Funktion EnterCriticalSection sollte am Anfang des kritischen Bereichs stehen und sorgt dafür das Threads mit den Eintritt warten solang der betroffene kritische Bereich gesperrt ist. Die Funktion LeaveCriticalSection gibt einen gesperrten kritischen Bereich wieder frei.
Ein simples Beispiel:

CPP:
#include <windows.h>

CRITICAL_SECTION handle;

// ...

bool Geldtransfer( unsigned int value )
{
   EnterCriticalSection( &handle );
   
   // Process Data
   
   LeaveCriticalSection( &handle );
}

int main()
{
   InitializeCriticalSection( &handle );
   
   // ...

   DeleteCriticalSection( &handle );
}


Es ist allerdings zu erwähnen das kritische Bereiche nur innerhalb eines Prozesses funktionieren. Für Prozessübergreifende Synchronisation muss eine andere Lösung gefunden werden. Welche das ist wird im nächsten Abschnitt besprochen.

2.3 Mutexe
Mutexe (mutually exclusive) sind Synchronisationsobjekte die, anders als kritische Bereiche, auch Prozessübergreifend zur Synchronisation eingesetzt werden können. Das Verhalten unterscheidet sich aber sonst nicht von kritischen Bereichen, es wird auch hier genau einem Thread erlaubt auf die geschützten Daten zuzugreifen. Eigentlich können Mutexe auch statt kritischen Bereichen eingesetzt werden. Da diese aber mehr Overhead haben, wegen des Management für Prozessübergreifende Funktionalität, sind diese langsamer als kritische Bereiche. Wenn man also genau weiß das keine Daten zwischen mehreren Prozessen geteilt werden so sollte man, falls vorhanden, lieber kritische Bereiche verwenden als Mutexe.
Mutexobjekte werden u.A. von der WinAPI angeboten, aber auch von anderen APIs. Hier werden die Funktionen der WinAPI und die von POSIX angebotenen Funktionen vorgestellt. Die gesammte Handhabung unterscheidet sich allerdings kaum von der bei kritischen Bereichen.

CPP:
HANDLE WINAPI CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  BOOL bInitialOwner,
  LPCTSTR lpName
);

Um einen Mutex zu erstellen wird die Funktion CreateMutex verwendet. Diese erwartet drei Parameter. Zum einen einen Zeiger zu einem Objekt vom Typ SECURITY_ATTRIBUTES. Dieser Zeiger kann NULL sein. Sollte dies der Fall sein wird ein „default security descriptor“ verwendet und dem Mutex zugewiesen. Die Struktur hat folgende Deklaration.

CPP:
typedef struct _SECURITY_ATTRIBUTES {
    DWORD  nLength;
    LPVOID lpSecurityDescriptor;
    BOOL   bInheritHandle;
} SECURITY_ATTRIBUTES;


Wenn der zweite Parameter wahr ist so übernimmt der aufrufende Thread, solang er das Mutexobjekt erzeugt, den Besitz des Mutexobjekts automatisch. Andernfalls übernimmt der aufrufende Thread nicht den Besitz.
Der letzte Parameter bestimmt den Namen des Mutex. Um einen namenlosen Mutex zu erstellen kann hier einfach ein Null-Zeiger übergeben werden.
Der Rückgabeparameter ist ein Handle auf den erstellten Mutex. Sollte ein Fehler in irgendeiner Form aufgetreten sein wird '0' zurückgegeben und der Fehlercode kann mit GetLastError() abgefragt werden.
Um einen Mutex zu löschen verwendet man die Funktion CloseHandle().

CPP:
BOOL CloseHandle(
  HANDLE hObject
);


Die Funktion WaitForSingleObject übernimmt das stoppen von zugreifenden Threads auf einen gesperrten Bereich.

CPP:
DWORD WINAPI WaitForSingleObject(
  HANDLE hHandle,
  DWORD dwMilliseconds
);


Ihr wird zum einen der Handle des Mutexes übergeben zum anderen muss ein timeout Wert angegeben werden. Ist die angegebene Zeitspanne abgelaufen kehrt die Funktion zurück ohne etwas zu bewirken. Bei Verwendung von WaitForSingleObject im Multithreadingbereich macht es allerdings Sinn INFINITE als Timeoutwert zu übergeben. Hiermit wartet die Funktion ohne Rücksprung auf die Freigabe des Mutex.
Um einen gesperrten Mutex wieder freizugeben wird die Funktion ReleaseMutex() verwendet.

CPP:
BOOL WINAPI ReleaseMutex(
  HANDLE hMutex
);


Hier muss lediglich der Handle des Mutex übergeben werden. Im Fehlerfall liefert die Funktion den Wert „FALSE“ zurück. Der Fehlercode kann mit GetLastError() angefragt werden.

Die POSIX API bietet aber Äquivalenten zu den WinAPI Mutexfunktionen. Diese werden hier kurz aufgelistet.

CPP:
WinAPI, POSIX, Funktion
CreateMutex, pthread_mutex_init, Mutex erstellen
CloseHandle, pthread_mutex_destroy, Mutex löschen
WaitForSingleObject, pthread_mutex_lock, Mutex sperren
ReleaseMutex, pthread_mutex_unlock, Mutex entsperren


Zum Schluss noch das letzte Beispiel unter Verwendung von Mutexen.

CPP:
#if defined( _WIN32 )
   #ifndef WIN32_LEAN_AND_MEAN
      #define WIN32_LEAN_AND_MEAN
   #endif

   #include <windows.h>

   HANDLE mutex;
#elif defined( POSIX )
   #include <pthread.h>

   pthread_mutex_t mutex;
#endif

// ...

bool Geldtransfer( unsigned int value )
{
#if defined( _WIN32 )
   WaitForSingleObject( mutex, INFINITE );
#elif defined( POSIX )
   pthread_mutex_lock( &mutex )
#endif
   
   // Process Data

#if defined( _WIN32 )
   ReleaseMutex( mutex );
#elif defined( POSIX )
   pthread_mutex_unlock( &mutex );
#endif
}

int main()
{
#if defined( _WIN32 )
   mutex = CreateMutex( 0, FALSE, 0 );
#elif defined( POSIX )
   pthread_mutex_init( &mutex );
#endif
   
   // ...

#if defined( _WIN32 )
   ReleaseMutex( mutex );
   CloseHandle( mutex );
#elif defined( POSIX )
   pthread_mutex_destroy( &mutex );
#endif
}


2.4 Semaphoren
Semaphoren unterscheiden sich von kritischen Bereichen und Mutexen. Sie werden normal dann eingesetzt wenn nicht nur ein Thread zeitgleich auf eine Ressource zugreifen darf sondern gleich mehrere. Prinzipiell funktioniert eine Semaphore wie ein Zähler der von den zugreifenden Threads inkrementiert und dekrementiert werden kann. Ist der Zähler des Semaphor auf Null angelangt so müssen alle weiteren Threads mit ihrem Zugriff warten bis ein aktiver Thread beendet ist und somit den Zähler wieder inkrementiert.
Hierfür bietet die WinAPI wieder einige Funktionen. Um ein Semaphor Objekt zu erstellen nutzt man üblicherweise die Funktion CreateSemaphore().

CPP:
HANDLE WINAPI CreateSemaphore(
  LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
  LONG lInitialCount,
  LONG lMaximumCount,
  LPCTSTR lpName
);


Der erste Parameter wird gehandhabt wie bei der Funktion CreateMutex() (s.o.). Die beiden nächsten Parameter geben an wie viel Threads gleichzeitig auf die geschützten Ressourcen Zugriff haben dürfen. Jedesmal wenn ein Thread auf die Ressource zugreift wird der Zähler dekrementiert, gibt der Thread die Semaphore frei wird der Zähler inkrementiert.
Um ein Semaphorobjekt zu löschen nutzt man, äquivalent zu Mutexen, die Funktion CloseHandle() (s.o.). Genauso verwendet man WaitForSingleObject() um den Besitz des Semaphor zu übernehmen und dessen Zähler zu dekrementieren.
Zum ein Semaphorobjekt freizugeben verwendet man üblicherweise die Funktion
ReleaseSemaphore().

CPP:
BOOL WINAPI ReleaseSemaphore(
  HANDLE hSemaphore,
  LONG lReleaseCount,
  LPLONG lpPreviousCount
);


Der erste Parameter ist der Handle des initialisierten Semaphor. Der zweite Parameter gibt die Anzahl an um welchen der Semaphorzähler inkrementiert werden soll. Überschreitet der inkrementierte Zähler das gegebene Maximum (beim Initialisieren) so ändert sich der Zähler nicht und die Funktion gibt den Wert „FALSE“ zurück.
Dem letzten Parameter kann ein Zeiger auf ein LONG Objekt übergeben werden, dieses erhält dann den Wert des uninkrementierten Zählers, alternativ ist eine Übergabe eines Null-Zeigers möglich.

Im Folgenden eine Liste der Funktionen, der WinAPI, zum Behandeln von Semaphoren und deren POSIX Äquivalenten.

CPP:
WinAPI, POSIX, Funktion
CreateSemaphore, sem_init, Semaphor erzeugen
CloseHandle, sem_destroy, Semaphor löschen
WaitForSingleObject, sem_wait, Semaphor übernehmen
ReleaseSemaphore, sem_post, Semaphore freigeben


2.5 Events
Das letzte der vier Synchronisationsobjekte ist das Ereignis (Event). Es ist darauf ausgelegt unnötige Prozessorzeit in Threads zu unterbinden. Je mehr Threads zeitgleich ausgeführt werden desto langsamer ist die Ausführung des einzelnen Threads.
Threads die nichts zu tun haben sollten eigentlich keine Prozessorzeit verbrauchen und sollten somit, bis sie wieder eine Aufgabe bekommen, blockiert werden. Die dadurch gewonnene Prozessorzeit kann dann auf die aktiven Threads aufgeteilt werden um diese zu beschleunigen.
Ereignisse sind dafür verantwortlich den Threads mitzuteilen wann diese wieder aktiv werden dürfen. Jeder Thread teilt einem Ereignis mit das er auf ein Signal wartet um seine Arbeit fortzusetzen. Sobald das Ereignis dieses Signal gibt nimmt jeder Thread seine Arbeit an genau der Position auf an welcher er in den Ruhezustand gegangen ist.
Die WinAPI bietet auch hierfür einige Funktionen.

CPP:
HANDLE WINAPI CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL bManualReset,
  BOOL bInitialState,
  LPCTSTR lpName
);


Der erste Parameter unterscheidet sich nicht von den vorhin vorgestellten Funktionen CreateMutex() und CreateSemaphore().
Der zweite Parameter gibt an ob der Event automatisch seinen Status ändert oder ob dies manuell geschehen soll. Im manuellen Modus muss die Funktion ResetEvent() aufgerufen werden, wenn das Objekt seinen Status automatisch ändern soll tut es das immer dann wenn ein wartender Thread freigegeben wurde.
Der dritte Parameter setzt den Initialwert auf signaled bzw nonsignaled, je nach Wertübergabe.
Über den vierten Parameter kann dem Event ein Name zugewiesen werden, dies ist allerdings nicht notwendig. Soll ein namenloser Event erzeugt werden kann einfach ein Null-Zeiger übergeben werden.
Um das Eventobjekt zu zerstören nutzt man, äquivalent zu Mutex und Semaphore, die Funktion CloseHandle().
Die Funktonen ResetEvent() und SetEvent() werden verwendet um den Status des Eventobjekts zu setzen. ResetEvent() setzt den Status auf nonsigaled und SetEvent() auf signaled. Die Deklaration der Funktionen sieht aus wie folgt.

CPP:
BOOL WINAPI ResetEvent(
  HANDLE hEvent
);

BOOL WINAPI SetEvent(
  HANDLE hEvent
);


Die Funktionen WaitForSingleObject() und WaitForMultipleObject() werden verwendet um den Thread im Ruhezustand zu belassen bis er eine konkrete Anweisung des Eventobjekts bekommt.
Eine Liste der WinAPI Funktionen sowie der POSIX Äquivalenten gibt es hier.

CPP:
WinAPI, POSIX, Funktion
CreateEvent, pthread_cond_init, Event erzeugen
CloseHandle, pthread_cond_destroy, Event löschen
WaitForSingleObject, pthread_cond_wait, Auf Eventsignal warten
PulseEvent, pthread_cond_broadcast,  Alle wartenden Threads aktivieren


So, genug der trockenen Theorie! Achja, Rechtschreibfehler dürfen alle behalten werden!!! Smile

grüße
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden
Beiträge der letzten Zeit anzeigen:   
Neues Thema eröffnen   Neue Antwort erstellen    JLI Spieleprogrammierung Foren-Übersicht -> Tutorials Alle Zeiten sind GMT
Seite 1 von 1

 
Gehe zu:  
Du kannst keine Beiträge in dieses Forum schreiben.
Du kannst auf Beiträge in diesem Forum nicht antworten.
Du kannst deine Beiträge in diesem Forum nicht bearbeiten.
Du kannst deine Beiträge in diesem Forum nicht löschen.
Du kannst an Umfragen in diesem Forum nicht mitmachen.


Powered by phpBB © 2001, 2005 phpBB Group
Deutsche Übersetzung von phpBB.de

Impressum