Threads:
Was ist ein Thread? Dieses englische Wort bedeutet "Faden". Wenn eine Anwendung also mehrere Threads besitzt, dann laufen mehrere Programm-"Fäden" zeitlich parallel. Man spricht dann von Multithreading im Gegensatz zu Singlethreading. Die Fragen, die sich stellen, sind:
Wenn man im Betriebssystem MS Windows eine Anwendung startet, dann wird ein Prozess gestartet. Dieser Prozess beginnt mit einem Thread. Dieser Thread kann seit den 32-Bit-Versionen des Betriebssystems (also seit Windows 95) weitere Threads hervorbringen. Das Betriebssystem muß dafür sorgen, dass die Rechenzeit ständig in kleinen Zeitscheiben zwischen all den Threads aufgeteilt wird. Bei Multithreading laufen die Threads sozusagen "quasi-parallel", oder genauer gesagt: es können nur soviele Threads "echt" parallel laufen, wie der Computer Prozessoren hat.
Geeignet sind weitere Threads für die Erledigung rechenintensiver Hintergrundaufgaben. Der Haupt-Thread kümmert sich während seiner Zeitscheiben um den "mausklickenden" Benutzer, der rasche Reaktionen verlangt, während weitere Threads im Hintergrund in ihren Zeitscheiben Haupt- oder Nebenarbeiten erledigen. Hier liegt der entscheidende Vorteil dieser Methode.
Multithreading-Anwendungen sind einfach und kompliziert zugleich. Einfach, weil sie leicht zu programmieren sind, kompliziert, weil Threads sich zeitlich unkoordiniert - also völlig asynchron - verhalten. Man muß sich daher neben der eigentlichen Programmieraufgabe verstärkt um die Regeln für das Miteinander - also die Synchronisierung der Threads - kümmern. Dafür kann sich das Antwortverhalten einer Anwendung signifikant erhöhen. Dies ist der Anreiz für die Mühe. Eine Geschwindigkeitserhöhung des gesamten Prozesses ist bei einer Maschine mit einem einzigen Prozessor nicht zu erwarten, eher das Gegenteil aufgrund des Multithreading-Überbaus.
Als Analogie stellt man sich z.B. ein
Ladengeschäft
mit angrenzender Werkstatt vor. Ist nur eine Person anwesend, muß
diese, wenn ein Kunde den Laden betritt, die Arbeit in der Werkstatt
unterbrechen
und in den Laden eilen. Sind mehrere Personen anwesend, kann man die
Bedienung
der Kunden und die Werkstattarbeit personell sinnvoll aufteilen. Das
Multithreading-Konzept
hat sein Vorbild also in der arbeitsteiligen Gesellschaft mit den damit
verbundenen Koordinationsaufgaben.
Nun zur zweiten Frage.
Wie sieht eigentlich ein einfaches MFC-Multithreading-Programm aus?
Wir erzeugen zunächst eine dialogbasierende MFC-Anwendung namens "Thread001" mit zwei Schaltflächen und zwei damit verbundenen Funktionen (OnButtonStart und OnButtonStop). Der Klick auf den einen Button ( IDC_BUTTONSTART, Aufschrift: "Thread starten" ) soll einen zusätzlichen Hintergrund-Thread starten, während der Klick auf den zweiten Button ( IDC_BUTTONSTOP, Aufschrift: "Thread stoppen" ) diesen Thread stoppt.
In der Klassendefinition der Dialogklasse (CThread001Dlg) werden wir zwei Member (z.B. mit Assistent) hinzufügen:
Wir fügen
nun
folgenden Start- und Stopp-Code und den Code der Thread-Funktion hinzu:
class
CThread001Dlg
: public CDialog { // Konstruktion public: static UINT thrFunction (LPVOID pParam); CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor ... ... private: int m_Flag; } //... void
CThread001Dlg::OnButtonStart() void
CThread001Dlg::OnButtonStop() UINT
CThread001Dlg::thrFunction(LPVOID pParam) |
Nach dem Kompilieren sollte das Multithreading bereits funktionieren. Wie können wir uns nun vergewissern, dass neben dem Hauptthread wirklich ein zweiter Thread erzeugt wird? Erstens "hören" wir den Thread, da jede Sekunde ein Sound durch MessageBeep(0) ausgelöst wird. Zweitens können wir vor und nach dem Thread-Start den Prozess-Viewer, ein Tool, das den MS VC++ 6 begleitet, einsetzen, um die Anzahl der Threads je Prozess zu prüfen:
Vor dem Start des zweiten Threads:
Nach dem Start des zweiten Threads:
Zunächst analysieren wir den Code, damit das Zusammenspiel zwischen den Threads klarer wird:
Die entscheidende Zeile für den Start des zusätzlichen Arbeitsthreads ist:
CWinThread* pThread = AfxBeginThread (thrFunction, &m_Flag);
Wir starten hier einen sogenannten
Arbeitsthread
("Workerthread", ohne Fenster, ohne Nachrichtenschleife).
Die vollständige Syntax dieser
Funktion
lautet:
CWinThread*
AfxBeginThread ( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); |
Der Rückgabewert der Funktion ist ein Zeiger auf das erstellte Thread-Objekt der MFC-Klasse CWinThread.
Der erste Parameter ist ein Zeiger auf die eigentlich Threadfunktion. Diese Funktion muß wie folgt deklariert werden:
UINT ThreadFunction( LPVOID );
Der Funktionsname kann natürlich frei gewählt werden.
Der zweite Parameter ist ein Zeiger auf den dieser Threadfunktion zu übergebenden Parameter.
Wir übergeben in unserem Beispiel die Integerzahl 1 an diese Funktion. Der Parameter ist vom Zeiger-Typ LPVOID. Daher erfolgt in der Threadfunktion die Zurückumwandlung von void* auf int*. Den Inhalt dieser Adresse erhalten wir dann mit *pFlag. *pFlag ist in diesem Fall also identisch mit m_Flag. Daher können wir hier den Thread durch einfaches Nullsetzen von m_Flag beenden.
Der dritte Parameter legt die Priorität dieses Threads fest. Übergeben wir hier den Wert 0, dann hat der zusätzliche Thread die gleiche Priorität wie der erzeugende Thread.
Werte geordnet nach abnehmender
Priorität
sind:
THREAD_PRIORITY_TIME_CRITICAL
THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_LOWEST THREAD_PRIORITY_IDLE |
Im Standardfall ist dieser Parameter automatisch auf THREAD_PRIORITY_NORMAL gesetzt. Das reicht für normale Anwendungen völlig aus.
Der vierte Parameter legt die Größe des Stacks in Bytes für den neuen Thread fest. Ein Wert von 0 ergibt die gleiche Stack-Größe wie beim erzeugenden Thread (normal sind max. 1 MB).
Der fünfte Parameter spezifiziert
die Erzeugung des Threads, hier gibt es zwei Möglichkeiten:
CREATE_SUSPENDED: | Der Thread wird von außen durch ResumeThread() gestartet. |
0: | Der Thread wird sofort gestartet. |
Der sechste Parameter ist ein Zeiger
auf
eine SECURITY_ATTRIBUTES Struktur.
Wenn dieser Zeiger gleich NULL ist, dann
gelten die gleichen Sicherheitsattribute wie beim erzeugenden Thread.
"MFC under the hood":
Wen es interessiert, die
Implementierung
dieser MFC-Funktion findet sich in THRDCORE.CPP :
CWinThread*
AFXAPI AfxBeginThread(AFX_THREADPROC
pfnThreadProc, LPVOID pParam, int nPriority, UINT nStackSize, DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs) { //... ASSERT(pfnThreadProc != NULL); CWinThread*
pThread
= DEBUG_NEW CWinThread(pfnThreadProc, pParam); if
(!pThread->CreateThread(dwCreateFlags|CREATE_SUSPENDED,
nStackSize, lpSecurityAttrs)) return
pThread; |
Sie sehen, der erste Einstieg in die Thread-Programmierung ist mittels MFC nicht allzu schwierig.
Damit Sie Tests mit Benutzereingaben
durchführen
können, fügen wir folgende Funktion für die Nachricht
WM_MOUSEMOVE
hinzu:
void
CThread001Dlg::OnMouseMove(UINT nFlags, CPoint point)
{ CClientDC dc(this); dc.SetPixel(point, RGB(0,0,0)); CDialog::OnMouseMove(nFlags,
point); |
Nun können Sie durch Ziehen mit der Maus über das Dialogfeld ein Gefühl für das Antwortverhalten Ihrer Anwendung mit und ohne laufenden zweiten Thread erhalten. Sie sollten im konkreten Fall keinen Unterschied sehen.
Wir haben die Member-Variable m_Flag auf dem Umweg über mehrere Zeiger zur Verwendung in unserer Threadfunktion weitergereicht. Hier gibt es eine interessante Alternative, mit der wir den Zwang einer statischen Funktion quasi aushebeln:
Im ersten Schritt fügen wir eine
zusätzliche
nicht-statische
Member-Funktion void CThread001Dlg::thrRun() hinzu.
class
CThread001Dlg
: public CDialog { // Konstruktion public: void thrRun(); static UINT thrFunction (LPVOID pParam); CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor ... ... |
Im zweiten
Schritt
wechseln wir nach OnButtonStart() und übergeben dort als zweiten
Parameter
in AfxBeginThread(...) keinen Zeiger auf m_Flag, sondern den this
-Zeiger,
den wir nachfolgend in der statischen Funktion in einen Zeiger auf
unsere
Dialogklasse CThread001Dlg* umwandeln. Mit diesem Zeiger können
wir
dann die eigentliche nicht-statische Funktion starten. Die
Member-Variable
kann dort direkt verwendet werden.
void
CThread001Dlg::OnButtonStart() { m_Flag = 1; CWinThread* pThread = AfxBeginThread (thrFunction, this); } void
CThread001Dlg::OnButtonStop() UINT
CThread001Dlg::thrFunction(LPVOID pParam) void
CThread001Dlg::OnMouseMove(UINT nFlags, CPoint point)
CDialog::OnMouseMove(nFlags, point); void
CThread001Dlg::thrRun() |
Nach diesem Schema lassen sich auch
leicht
zwei oder mehr zusätzliche Threads erzeugen. Ändern Sie
einfach
den Sourcecode ohne Einsatz des Assistenten wie folgt ab:
class
CThread001Dlg
: public CDialog { // Konstruktion public: void thrRun1(); void thrRun2(); static UINT thrFunction1 (LPVOID pParam); static UINT thrFunction2 (LPVOID pParam); CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor //... //... private: int m_Flag; }; void
CThread001Dlg::OnButtonStart() void
CThread001Dlg::OnButtonStop() UINT
CThread001Dlg::thrFunction1(LPVOID pParam) UINT
CThread001Dlg::thrFunction2(LPVOID pParam) void
CThread001Dlg::OnMouseMove(UINT
nFlags, CPoint point)
CDialog::OnMouseMove(nFlags,
point); void
CThread001Dlg::thrRun1() } void
CThread001Dlg::thrRun2() |
Dass diese Anwendung wirklich 3 Threads umfaßt, zeigt uns erneut der Process Viewer:
Mit zwei zusätzlichen
Arbeitsthreads
kann man experimentell zwei gegensätzliche Aktionen testen. Hier
ein
kleines Beispiel mit einem Checkbutton (vergrößern Sie bitte
das Dialogfeld, fügen Sie ein Optionsfeld hinzu und
erzeugen
Sie die Member-Variable m_Checkbutton vom Kontroll-Typ CButton.):
void
CThread001Dlg::thrRun1() { while (m_Flag) { Sleep(99); m_Checkbutton.SetCheck(TRUE); } } void
CThread001Dlg::thrRun2() |
Das ergibt ein ständiges Hin und
Her
mit dem Setzen/Zurücksetzen des Optionsfeldes mit zeitlich
bedingten
Effekten.
Was passiert eigentlich, wenn man beide
Threads direkt gegeneinander loshetzt, also ohne Sleep(...)?
void
CThread001Dlg::thrRun1() { while (m_Flag) { m_Checkbutton.SetCheck(TRUE); } } void
CThread001Dlg::thrRun2() |
Es ergibt sich - wie oben - ein
ständiges
Hin und Her, ohne daß sich ein Thread durchsetzen kann. Das ist
ein
interessanter Punkt. Hier können wir die Threadprioritäten
ins
Spiel bringen. Nutzen Sie folgenden Code, um die Prioritäten der
beiden
Threads unterschiedlich zu gestalten:
void
CThread001Dlg::OnButtonStart() { m_Flag = 1; CWinThread* pThread1 = AfxBeginThread (thrFunction1, this); CWinThread* pThread2 = AfxBeginThread (thrFunction2, this); pThread1->SetThreadPriority(THREAD_PRIORITY_HIGHEST);
|
Das führt nun zu einer klaren -
optisch
gut nachvollziehbaren - Vorherrschaft des einen Threads gegenüber
dem anderen.
Wenn Sie den Hauptthread ( Zeiger pThread0
) in diese Experimente einbeziehen wollen, benutzen Sie AfxGetThread():
"MFC under the hood":
Hier die einfache Implementierung von
AfxGetThread() in THRDCORE.CPP:
CWinThread*
AFXAPI AfxGetThread() { // check for current thread in module thread state AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState(); CWinThread* pThread = pState->m_pCurrentWinThread; //
if no CWinThread for the module, then use the global app
return
pThread; |
Hier unsere Implementierung in
OnButtonStart:
void
CThread001Dlg::OnButtonStart() { m_Flag = 1; CWinThread*
pThread0 = AfxGetThread( ); pThread0->SetThreadPriority(THREAD_PRIORITY_ABOVE_NORMAL);
|
Wenn die Priorität des Hauptthreads zu niedrig wird, reagiert die Anwendung schlicht und einfach nicht mehr auf Benutzereingaben. Dann müssen Sie die Anwendung mit dem Hammer (z.B. Taskmanager) schließen.
Nach diesen einfachen Experimenten
haben
Sie nun ein praktisches Verständnis, wie man einen oder mehrere
Arbeitsthreads
in MFC startet und entsprechende Thread-Funktionen erstellt.
Threaderzeugung mit WinAPI - "back to the roots"
Lassen Sie uns zur Vertiefung noch
einen
Kurzausflug zur
WinAPI-Programmierung
(ohne MFC) machen.
Dort erfolgt Multithreading sehr ähnlich. Hier ein einfaches
Beispiel
zur Veranschaulichung und zum Ausprobieren:
#include
<windows.h> #include <process.h> /* _beginthread, _endthread ... */ LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int Flag = 1; int WINAPI
WinMain (HINSTANCE
hInstance, HINSTANCE hPrevInstance,
wndclass.style =
CS_HREDRAW
| CS_VREDRAW ;
RegisterClass (&wndclass);
ShowWindow (hwnd, iCmdShow) ;
while (GetMessage (&msg, NULL, 0, 0)) VOID
Thread (PVOID pvoid) LRESULT
CALLBACK WndProc
(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
case WM_RBUTTONDOWN: /* stoppt Thread */
case
WM_DESTROY: |
Für _beginthread(...) benötigt man process.h. In diesem Header findet man folgende Deklarationen:
/*
function prototypes */
#ifdef _MT
_CRTIMP unsigned
long
__cdecl _beginthread (void (__cdecl *) (void *),
unsigned, void *);
_CRTIMP void __cdecl
_endthread(void);
_CRTIMP unsigned long
__cdecl _beginthreadex(void *, unsigned,
unsigned (__stdcall *) (void *), void *, unsigned, unsigned *);
_CRTIMP void __cdecl
_endthreadex(unsigned);
#endif
Bei den Projekteinstellungen für C/C++ müssen Sie unter Code Generation die Laufzeit-Bibliothek auf Multithreaded umstellen, damit das Compiler Flag /MT gesetzt wird, ansonsten gilt _beginthread(...) als nicht deklariert:
Wie Sie sehen, nimmt die MFC-Programmierung Ihnen diesbezüglich einige Kleinarbeiten ab.
Auch bei Konsolen kann man die
Thread-Programmierung,
sinnvoll z.B. für Server, realisieren. Darauf wollen wir hier
jedoch
nicht eingehen, da dies keine neuen Aspekte bringt.
Thread-Synchronisierung
Das zentrale Thema bei Multithreadinganwendungen ist die sogenannte Synchronisierung. Das ist die zeitliche Steuerung der Threads. Warum braucht man diese Technik? Stellen Sie sich einfach die Arbeitsteilung im normalen Leben vor und lassen Sie vor Ihrem geistigen Auge folgende Vorgänge gleichzeitig ablaufen:
An einer Kreuzung fahren alle Fahrzeuge
gleichzeitig los.
In einer erregten Diskussion sprechen
alle zur gleichen Zeit.
Sie essen vom Teller, während der
Kellner darauf serviert.
Sie trinken aus dem Glas, in das der
Kellner
gerade eingießt.
Sie schließen eine Tür,
während
jemand durchgehen will.
Zwei Personen bedienen gleichzeitig
unkoordiniert
einen Computer.
Es gibt nur drei gleiche Werkzeuge (z.B.
Bohrmaschine), aber zehn Handwerker, die diese gleichzeitig
benützen
wollen.
In einem Programm sieht dieses Problem
z.B. so aus:
Thread1 und Thread 2 schreiben in eine
String-Variable, während Thread3 diese liest und ausgibt. Was wird
der Benutzer sehen? Den String von Thread 1 oder von Thread 2? Beides
ist
möglich. Vielleicht kommt es auch zu einem Durcheinander oder gar
zu einem Programmabsturz.
Probieren wir es aus:
Erzeugen Sie ein Dialog-Programm mit
einem
Button für Start ("Thread starten") und einen Button für Stop
("Thread stoppen") und zwei Edit-Feldern. Den Edit-Feldern ordnen Sie
bitte
mittels Klassenassistent zwei Member-Variablen vom Typ CString zu:
m_strEdit1 und m_strEdit2. Thread
1 und Thread 2 werden beide in m_strEdit1 schreiben, das dem obere
Edit-Feld
zugeordnet ist.
Thread 3 liest m_strEdit1 aus, weist es
m_strEdit2 zu, das im unteren Edit-Feld erscheint.
Die Header-Datei sollte bitte wie folgt
aussehen:
Wir finden dort statische und
nicht-statische
Member-Funktionen für die drei Arbeitsthreads, die
Member-Variablen
für die Edit-Felder, die Funktionen OnButtonStart() und
OnButtonStop()
sowie unsere Variable m_Flag.
class
CThread001Dlg
: public CDialog { // Konstruktion public: void thrRun1(); void thrRun2(); void thrRun3(); static UINT thrFunction1 (LPVOID pParam); static UINT thrFunction2 (LPVOID pParam); static UINT thrFunction3 (LPVOID pParam); CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor //
Dialogfelddaten //
Vom Klassenassistenten generierte Überladungen virtueller
Funktionen //
Implementierung //
Generierte Message-Map-Funktionen private:
|
Die Implementierung unserer drei
Threads
inclusive Start und Stop sollte wie folgt aussehen:
void
CThread001Dlg::OnButtonStart()
{ m_Flag = 1; CWinThread* pThread1 = AfxBeginThread (thrFunction1, this); CWinThread* pThread2 = AfxBeginThread (thrFunction2, this); CWinThread* pThread3 = AfxBeginThread (thrFunction3, this); } void
CThread001Dlg::OnButtonStop()
UINT
CThread001Dlg::thrFunction1(LPVOID
pParam) return
0; UINT
CThread001Dlg::thrFunction2(LPVOID
pParam) return
0; UINT
CThread001Dlg::thrFunction3(LPVOID
pParam) return
0; void
CThread001Dlg::thrRun1()
void
CThread001Dlg::thrRun2()
void
CThread001Dlg::thrRun3()
|
Nun unternehmen wir folgendes
Experiment:
Wir lassen die Threads mehrere Male
abwechselnd
starten und stoppen, um den von Arbeitsthread 3 ausgelesenen String zu
bewundern:
Das sind die geordneten Zustände:
... und hier regiert das kreative Chaos:
Ich hoffe, das klappt bei Ihnen auch.
Die Aufgabe ist klar: Zuerst muß Thread1 oder Thread 2 sich zum Schreibvorgang anmelden, anschließend erhält nur einer der Threads die Freigabe zum Schreiben und zum Schluß meldet dieser sich wieder ordentlich ab. Erst jetzt erhält der wartende Thread 2 die Freigabe zum Schreiben oder der wartende Thread 3 die Freigabe zum Lesen. Entweder schreibt Thread 1 oder Thread 2 in den String, aber nicht beide gleichzeitig, und wenn geschrieben wird, wird nicht gelesen, und umgekehrt. So muß das ablaufen! Wir als Programmierer müssen die Oberhand über die "Taktsteuerung", die man bei Threads "Synchronisation" nennt, behalten. Ansonsten machen die Threads in unserem Programm, was sie wollen.
Wir benötigen einen Mechanismus
analog
dem Besuch einer Flugzeugtoilette. Die wartenden Personen sind die
angemeldeten
Threads, das Schloß bzw. Lichtzeichen (grün / rot) der
Toilette
ist der Verriegelungsmechanismus. Nur einer darf gleichzeitig rein, und
dann wird sofort verriegelt.
Kritische Abschnitte und Mutexe
Beide Hilfsmittel dienen als Verriegelungs-/Freigabemechanismus für den Zugriff mehrerer Threads auf die gleichen Variablen bzw. allgemein Ressourcen. Kritische Abschnitte kann man nur prozessintern einsetzen, während Mutexe auch prozessübergreifend funktionieren. Dafür sind Kritische Abschnitte "schlanker" und "schneller".
Zur Realisierung der kritischen
Abschnitte
steht in MFC die Klasse CCriticalSection bereit.
CCriticalSection
kennt nur drei Methoden:
den Konstruktor, Lock() und Unlock().
Zunächst muß man folgenden
Header
einbinden: #include <afxmt.h>
Wir erzeugen dann einen kritischen
Abschnitt,
d.h. ein Objekt der MFC-Klasse CCriticalSection. Der
Konstruktor
benötigt keinen Parameter.
Innerhalb der Funktion von Thread1 erfolgt
dann der Lock/Unlock-Mechanismus:
//z.B.
Member der Klasse CXXX oder global
CCriticalSection
cs;
void
CXXX::thrRun1()
{
cs.Lock();
//Aktionen
cs.Unlock();
}
Ein zweiter Thread kann erst dann erfolgreich cs.Lock() ausführen, wenn der erste Thread cs.Unlock() aufgerufen hat. Stellen Sie sich das vor wie bei dem Beispiel der Flugzeugtoilette. Das Türschloß ist cs. Mit cs.Lock() schließt man zu, und mit cs.Unlock() schließt man auf, und in der Zwischenzeit "erledigt man seine Geschäfte". Vor der Tür stehen die anderen Threads und warten, bis ihr eigenes cs.Lock() zum Zuge kommt.
Das realisieren wir sofort. Also was
müssen
wir machen? Zunächst einen kritischen Abschnitt einfügen. Man
kann das als globale Variable oder in unserem Fall auch als private
Member-Variable
erledigen. Wichtig ist, dass alle Threads darauf zugreifen können:
#include
<afxmt.h>
/////////////////////////////////////////////////////////////////////////////
class
CThread001Dlg :
public CDialog |
Jetzt haben wir einen "Schlüssel".
Nun müssen wir nur zum richtigen Zeitpunkt öffnen und
schließen.
Innerhalb der Thread-Funktionen wenden
wir das wie folgt konkret an:
void
CThread001Dlg::thrRun1() { while (m_Flag) { cs.Lock(); m_strEdit1 = "Jetzt"; m_strEdit1 += " schreibt"; m_strEdit1 += " Arbeitsthread Nr. 1"; cs.Unlock(); } } void
CThread001Dlg::thrRun2() void
CThread001Dlg::thrRun3() |
Nun ist endlich Ordnung eingekehrt bei der "quasiparallelen" Verwendung der Variable m_strEdit1. Einfach durch "Einklammern" mit Lock() / Unlock() einen kritischen Abschnitt schaffen und nur einen Thread zur gleichen Zeit seine Arbeit bezüglich dieser Variable erledigen lassen.
Es gibt da noch eine wichtige Feinheit, auf die ich hinweisen möchte. Die Prozessorauslastung unserer Mini-Anwendung liegt bei stolzen 100%. Überprüfen Sie es selbst mit dem Systemmonitor im Zubehör von MS Windows.
Preisfrage: Woran liegt das? Wie können wir dies ändern?
Der entscheidende Punkt ist unsere
while-Schleife.
Solange m_Flag gesetzt ist, laufen unsere Threads sozusagen Amok. Also
gönnen wir ihnen nach jeder Aktion eine Pause, damit die CPU sich
auch noch um den Rest der Welt kümmern kann. Wie wäre es mit
einer Millisekunde? Ist doch ausreichend Pause für einen Thread?
Threads
haben keinen Betriebsrat.
void
CThread001Dlg::thrRun1() { while (m_Flag) { cs.Lock(); //Aktion cs.Unlock(); Sleep(1); } } void
CThread001Dlg::thrRun2() void
CThread001Dlg::thrRun3() |
Wichtig ist, dass alle Threads mal Pause machen. Ein arbeitswütiger Thread kann den Prozessor alleine bei Laune halten.
Nun stellen Sie hoffentlich zwei Dinge
fest:
1) Die Anwendung läuft viel schneller
ab!
2) Die Prozessorauslastung liegt bei ca.
20%.
Während Sleep(...) verbraucht ein Thread keine Rechenleistung, sondern macht wirklich Pause.
Nachdem wir nun diese zwei wesentlichen Prinzipien untersucht haben, wenden wir uns einer komplexeren Anwendung zu. Da Multithreading sein Vorbild in unserer arbeitsteiligen Welt hat, verzichten wir auf weitere Mini-Beispiele und wenden uns sofort einer virtuellen Fertigungsstraße zu:
Das Modell unserer "virtuellen"
Fertigungsstraße
funktioniert wie folgt:
Der Rohstoffeinkauf beschafft 5
Einzelteile
für die Produktion. Die Vormontage fertigt aus Teil 1 und Teil 2
das
Zwischenprodukt Kombi A und aus Teil 3 und Teil 4 das Zwischenprodukt
Kombi
B. Die Endmontage produziert Endprodukt 1 aus Kombi A und Kombi B sowie
Endprodukt 2 aus Kombi B und Teil 5. Der Vertrieb bringt die beiden
Endprodukte
zu den Abnehmern. Die Endprodukte werden uns aktuell aus den
Händen
gerissen. Das Simulationsmodell soll helfen,
Kapazitätsengpässe
("bottle necks") zu finden und gleichzeitig die Vorräte zu
optimieren.
Für das zu erstellende Programm werden die jeweiligen Einzeltätigkeiten von Rohstoffeinkauf, Vormontage, Endmontage und Vertrieb in einzelnen Arbeitsthreads abgebildet. Der Bestand der Ressourcen wird als Zahlenvariablen vom Typ UINT dargestellt, auf die die einzelnen Threads zugreifen. Wenn Thread_Vormontage_KombiA abläuft, wird Bestand_Teil1 und Bestand_Teil2 um eins erniedrigt, während sich der Bestand_KombiA um eins erhöht. Wir müssen dafür sorgen, dass z.B. nicht Threads der Vormontage und Endmontage gleichzeitig auf die Bestandsvariable der Kombi A zugreifen können (Vormontage will erhöhen und Endmontage will erniedrigen). Hier muß ein kritischer Abschnitt (sprich ein "Schloß") dafür sorgen, dass nur jeweils ein Thread den Bestand von Kombi A verändert. Wir werden daher eine CCriticalSection cs_KombiA erzeugen, die hier als Wächter für den Zugriff von Thread_Vormontage_KombiA und Thread_Endmontage_Endprodukt1 auf den Bestand_KombiA fungiert.
Der Hauptthread hat die Aufgabe, ständig den aktuellen Gesamtzustand der Produktion zu visualisieren. Wir werden nur einen Start- und Stop-Button schaffen. Alles andere soll automatisch ablaufen.
Nun noch einige Details unseres
Produktionsmodells:
Die Beschaffung von Teilen erfolgt im
100er Pack und benötigt 60 Sekunden.
Die Vormontage von Kombi A benötigt
3,0 Sekunden.
Die Vormontage von Kombi B benötigt
2,0 Sekunden.
Die Endmontage von Endprodukt 1
benötigt
4,0 Sekunden.
Die Endmontage von Endprodukt 2
benötigt
5,0 Sekunden.
Der Vertrieb erfolgt im 10er Pack und
benötigt zur Bestandserniedrigung im Endprodukte-Lager jeweils 15
Sekunden.
In den Lagern darf nur jeweils ein Zu-
oder Abgang zur gleichen Zeit erfolgen.
Diese Fertigunszeiten bilden wir innerhalb des Threads auf einfache Weise mittels Sleep( Millisekunden ) ab. In dieser Zeit verbraucht der Thread keine Rechenzeit.
Verfolgen wir die Fertigung einer
KombiA
aus Teil 1 und Teil 2 im Programm:
class
CThread001Dlg
: public CDialog { // Konstruktion public: void thr_KombiA(); //... static UINT thrFunction_KombiA (LPVOID pParam); //... UINT m_Bestand_Teil1; //Klassenassistent UINT m_Bestand_Teil2; //Klassenassistent //... UINT m_Bestand_KombiA; //Klassenassistent //... private:
CCriticalSection
m_cs_Teil1,
m_cs_Teil2,
m_cs_Teil3, m_cs_Teil4, m_cs_Teil5, void
CThread001Dlg::OnButtonStart() void
CThread001Dlg::OnButtonStop() void
CThread001Dlg::OnTimer(UINT
nIDEvent) CDialog::OnTimer(nIDEvent);
UINT
CThread001Dlg::thrFunction_KombiA
(LPVOID pParam) return
0; void
CThread001Dlg::thr_KombiA()
while ( (m_Bestand_Teil1 > 0) && (m_Bestand_Teil2 > 0) )
m_cs_Teil1.Lock(); //Teil
1 wird aus dem Vorrat genommen
m_cs_Teil2.Lock(); //Teil
2 wird aus dem Vorrat genommen Sleep(3000); //Fertigungszeit
m_cs_KombiA.Lock(); //Kombi
A wird in den Vorrat genommen
m_Checkbutton_Vormontage_KombiA.SetCheck(FALSE); //Arbeitsvorgang
beendet void
CThread001Dlg::Warteschleife() |
Wir benutzen für den Zugriff auf die Bestandsdaten individuelle CCriticalSections. Damit ist sicher gestellt, dass nicht mehrere Threads simultan auf die gleiche Variable zugreifen. Jeder Zugriff wird in jedem Thread durch eine Lock()-Unlock()-Klammer eingeschlossen. Das ist alles.
Hinweis:
Wichtig ist auch
unsere kleine Member-Funktion "Warteschleife" innerhalb der
äußeren
while-Schleife, die dem "Rest des Computers" zumindest eine
Millisekunde
gewährt. Ohne diese Warteschleifen innerhalb der Threadfunktionen
sind diese ständig damit beschäftigt, die Bestandsdaten
abzufragen.
Ein wichtiges Detail. Probieren Sie es ohne Warteschleife aus und
beachten
Sie die 100%-Prozessorauslastung. Mit diesen Warteschleifen liegt der
CPU-Bedarf
des Programms bei ca. 5 %. Das können Sie selbst mit dem
"Systemmonitor"
(MS Windows-Zubehör) überprüfen und optimieren.
Unsere kleine Wirtschaftssimulation sieht optisch in der Basisversion wie folgt aus:
Die Produktion ist angelaufen.
Der
Rohstoffeinkauf
hat neue Teile besorgt, der Versand verschickt die ersten Endprodukte.
Der Process-Viewer zeigt uns an, dass wirklich 12 Threads gleichzeitig ablaufen. Diese sind:
Nachfolgend finden Sie zum vertieften
Studium der Thread-Synchronisierung das ganze Programm:
//StdAfx.h
#if _MSC_VER
> 1000 #define VC_EXTRALEAN // Selten verwendete Teile der Windows-Header nicht einbinden #include
<afxwin.h>
// MFC-Kern- und -Standardkomponenten #include <Afxmt.h> //{{AFX_INSERT_LOCATION}}
#endif //
!defined(AFX_STDAFX_H__65E8E946_9137_11D6_A393_004033E1CE3C__INCLUDED_)
//Resource.h //{{NO_DEPENDENCIES}}
// Next
default values
for new objects //Thread001.h : Haupt-Header-Datei für die Anwendung THREAD001 #if
!defined(AFX_THREAD001_H__65E8E942_9137_11D6_A393_004033E1CE3C__INCLUDED_)
#if _MSC_VER
> 1000 #ifndef
__AFXWIN_H__ #include "resource.h" // Hauptsymbole /////////////////////////////////////////////////////////////////////////////
class
CThread001App :
public CWinApp //
Überladungen // Implementierung //{{AFX_MSG(CThread001App)
///////////////////////////////////////////////////////////////////////////// //{{AFX_INSERT_LOCATION}}
#endif //
!defined(AFX_THREAD001_H__65E8E942_9137_11D6_A393_004033E1CE3C__INCLUDED_)
// Thread001Dlg.h : Header-Datei #if
!defined(AFX_THREAD001DLG_H__65E8E944_9137_11D6_A393_004033E1CE3C__INCLUDED_)
#if _MSC_VER
> 1000 /////////////////////////////////////////////////////////////////////////////
class
CThread001Dlg :
public CDialog void
thr_KombiA(); void
thr_Endprodukt1(); void
thr_VertriebEndprodukt1(); CThread001Dlg(CWnd* pParent = NULL); // Standard-Konstruktor //
Dialogfelddaten // Vom
Klassenassistenten
generierte Überladungen virtueller Funktionen //
Implementierung //
Generierte Message-Map-Funktionen CCriticalSection
m_cs_Teil1, m_cs_Teil2, m_cs_Teil3, m_cs_Teil4, m_cs_Teil5,
CWinThread*
pThread_Beschaffung_Teil1; CWinThread*
pThread_Vormontage_KombiA; CWinThread*
pThread_Endmontage_Endprodukt1; CWinThread*
pThread_Vertrieb_Endprodukt1; //{{AFX_INSERT_LOCATION}}
#endif //
!defined(AFX_THREAD001DLG_H__65E8E944_9137_11D6_A393_004033E1CE3C__INCLUDED_)
//
stdafx.cpp
: Quelltextdatei, die nur die Standard-Includes einbindet
#include
"stdafx.h" // Thread001.cpp : Legt das Klassenverhalten für die Anwendung fest. #include
"stdafx.h" #ifdef _DEBUG
/////////////////////////////////////////////////////////////////////////////
BEGIN_MESSAGE_MAP(CThread001App,
CWinApp) /////////////////////////////////////////////////////////////////////////////
CThread001App::CThread001App()
/////////////////////////////////////////////////////////////////////////////
CThread001App
theApp; /////////////////////////////////////////////////////////////////////////////
BOOL
CThread001App::InitInstance() //
Standardinitialisierung #ifdef _AFXDLL
CThread001Dlg
dlg; // Da
das Dialogfeld
geschlossen wurde, FALSE zurückliefern, so dass wir die
//
Thread001Dlg.cpp
: Implementierungsdatei #include
"stdafx.h" #ifdef _DEBUG
/////////////////////////////////////////////////////////////////////////////
class
CAboutDlg : public
CDialog //
Dialogfelddaten // Vom
Klassenassistenten
generierte Überladungen virtueller Funktionen //
Implementierung CAboutDlg::CAboutDlg()
: CDialog(CAboutDlg::IDD) void
CAboutDlg::DoDataExchange(CDataExchange*
pDX) BEGIN_MESSAGE_MAP(CAboutDlg,
CDialog) /////////////////////////////////////////////////////////////////////////////
CThread001Dlg::CThread001Dlg(CWnd*
pParent /*=NULL*/) void
CThread001Dlg::DoDataExchange(CDataExchange*
pDX) BEGIN_MESSAGE_MAP(CThread001Dlg,
CDialog) /////////////////////////////////////////////////////////////////////////////
BOOL
CThread001Dlg::OnInitDialog() // Hinzufügen des Menübefehls "Info..." zum Systemmenü. //
IDM_ABOUTBOX
muss sich im Bereich der Systembefehle befinden. CMenu*
pSysMenu
= GetSystemMenu(FALSE); SetIcon(m_hIcon,
TRUE); // Großes Symbol verwenden //Produktions-Hauptschalter
( 0 = off, 1 = on ) //Bestandsmengen
(hier: Stückzahl) UpdateData(FALSE); //Daten -> Felder return
TRUE;
// Geben Sie TRUE zurück, außer ein Steuerelement soll den
Fokus
erhalten void
CThread001Dlg::OnSysCommand(UINT
nID, LPARAM lParam) void
CThread001Dlg::OnPaint() void
CThread001Dlg::OnButtonStart() pThread_Beschaffung_Teil1
= AfxBeginThread (thrFunction_Teil1, this); pThread_Vormontage_KombiA
= AfxBeginThread (thrFunction_KombiA, this); pThread_Endmontage_Endprodukt1
= AfxBeginThread (thrFunction_Endprodukt1, this);
pThread_Vertrieb_Endprodukt1
= AfxBeginThread (thrFunction_VertriebEndprodukt1, this);
} void
CThread001Dlg::OnButtonStop() void
CThread001Dlg::OnTimer(UINT
nIDEvent) CDialog::OnTimer(nIDEvent);
////////////////////////////////////////
UINT
CThread001Dlg::thrFunction_Teil1
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_Teil2
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_Teil3
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_Teil4
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_Teil5
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_KombiA
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_KombiB
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_Endprodukt1
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_Endprodukt2
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_VertriebEndprodukt1
(LPVOID pParam) UINT
CThread001Dlg::thrFunction_VertriebEndprodukt2
(LPVOID pParam) ////////////////////////////////////////
void
CThread001Dlg::thr_Teil1() void
CThread001Dlg::thr_Teil2() void
CThread001Dlg::thr_Teil3() void
CThread001Dlg::thr_Teil4() void
CThread001Dlg::thr_Teil5() void
CThread001Dlg::thr_KombiA() void
CThread001Dlg::thr_KombiB() void
CThread001Dlg::thr_Endprodukt1() void
CThread001Dlg::thr_Endprodukt2() void
CThread001Dlg::thr_VertriebEndprodukt1() void
CThread001Dlg::thr_VertriebEndprodukt2() void
CThread001Dlg::Warteschleife()
//wichtig für niedrige Prozessorauslastung |
Dieses Programm können Sie weiterentwicklen oder auf eigene Abläufe umstricken. Entscheidend ist, dass Sie die Threaderzeugung und -steuerung klar erkennen. Multithreading unterstützt in unserem Fall ideal die Prozessorientierung.
Anmerkung:
Die Idee mit der
Flugzeugtoilette
als im Zugriff begrenzte Ressource, die ich hier gerne als Vergleich
darstelle,
hatte ich in der Tat unabhängig von Jeffrey Richter, aber beim
späteren
Durchblättern seines Buches "Programming Applications for MS
Windows"
fand ich in der 4. Auflage das gleiche Analogon, daher zitiere ich hier
das Original:
"What are the key points
to remember? When you have a resource that is accessed by multiple
threads,
you should create a CRITICAL_SECTION structure. Since I'm writing this
on an airplane flight, let me draw the following analogy. A
CRITICAL_SECTION
structure is like an airplane's lavatory, and the toilet is the data
that
you want protected. Since the lavatory is small, only one person
(thread)
at a time can be inside the lavatory (critical section) using the
toilet
(protected resource)."
Jeffrey Richter hat genau
so wenig wie ich im ersten Moment an Mütter mit kleinen Kindern
gedacht.
Die machen nicht einmal vor einem kritischen Abschnitt halt. Aber wir
werden
diesen Mehrfachzugriff auch noch mit Semaphoren abbilden (siehe unten).
Mutexe
Mutex ist ein zusammengesetzter Begriff, der von mutually exclusive (gegenseitig ausschließen) herrührt. Man verwendet einen Mutex analog zu kritischen Abschnitten mit dem Unterschied, dass Mutexe prozessübergreifend einsetzbar sind. Zuständig ist die MFC-Klasse CMutex. Verfügbare Funktionen sind: Konstruktor, Lock() und Unlock().
Der Konstruktor hat folgende Parameter:
CMutex
(
BOOL bInitiallyOwn = FALSE,
LPCTSTR lpszName = NULL,
LPSECURITY_ATTRIBUTES
lpsaAttribute
= NULL
);
Der Parameter bInitiallyOwn legt fest,
ob das Objekt CMutex sofort gesperrt werden soll (TRUE).
Der zweite Parameter gibt dem Objekt einen
Namen für die Kommunikation zwischen mehreren Prozessen.
Die Funktion Lock(...) kann bei CMutex einen Parameter aufnehmen, der die maximale Wartezeit in Millisekunden angibt. Dann wird automatisch "entriegelt". Dies ist ein entscheidender Unterschied zum kritischen Abschnitt. Bei der Interprozesskommunikation muß man eben härtere Bandagen anlegen, ansonsten könnte ein Prozess den anderen mit ins Verderben reißen. Dieser Parameter ist bei CCriticalSection::Lock(...) formal auch möglich. Dort wird der Parameter jedoch ignoriert!
Eine schnell erstellte Anwendung ist
einfach
der Ersatz von CCriticalSection durch CMutex in einer Anwendung.
Probieren
Sie es an einem unserer einfachen Beispiele aus:
class CThread001Dlg : public
CDialog { // Konstruktion public: void thrRun1(); ... static UINT thrFunction1 (LPVOID pParam); ... ... private: int m_Flag; CMutex cs; //anstelle CCriticalSection cs; }; |
Das zeigt, dass die Funktion als einfacher "Schlüssel" (ohne Namen) identisch ist. Da es sich hier jedoch um eine Anwendung handelt, die nicht prozessübergreifend arbeitet, ist der "Überbau" von CMutex nicht notwendig. CMutex arbeitet langsamer als CCriticalSection.
Also schauen wir uns für eine Anwendung von CMutex nach einem einfachen prozessübergreifenden Beispiel um:
Sie erstellen eine einfache
Dialoganwendung.
Ergänzen Sie Folgendes:
Zunächst fügen Sie ein Edit-Feld
hinzu. Als ID belassen Sie es bitte auf IDC_EDIT1. Zusätzlich
benötigen
wir noch eine Funktion für den rechten Mausklick, mit dem wir den
Arbeitsthread starten wollen. In die Datei xxxDlg.cpp fügen Sie
folgenden
Sourcecode hinzu:
//
Thread_mit_MutexDlg.cpp : Implementierungsdatei // #include
"stdafx.h" #include "afxmt.h" CMutex key( FALSE, "MeinSchluessel" ); UINT
thrFunction1(LPVOID pParam)
key.Lock(); ::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Wieder offen");
return 0; /////////////////////////////////////////////////////////////////////////////
class
CAboutDlg : public CDialog ... void
CThread_mit_MutexDlg::OnRButtonDown(UINT
nFlags, CPoint point) |
Wir definieren also in diesem Fall
einen
globalen Mutex und eine globale Thread-Funktion thrFunction1(...).
Entscheidend
ist, dass der Mutex einen Namen (hier: "MeinSchluessel") erhält.
Damit
ist er systemweit bekannt. Wir haben nun einen
prozessübergreifenden
Schlüssel.
Der Thread wird durch den rechten
Mausklick
gestartet.
Nun sind wir bereit für unser Experiment. Stellen Sie sich einfach vier wartende Personen (wir benutzen Threads an ihrer Stelle) vor einer Flugzeugtoilette vor. Jeder benötigt 5 Sekunden. Also wie machen wir das? Ganz einfach: Starten Sie die Anwendung vier Mal und verteilen Sie die Fenster in der gestarteten Reihenfolge gleichmäßig auf dem Bildschirm: Zuerst links oben, dann rechts oben, dann links unten und zum Schluß rechts unten . Dann klicken Sie schnell hintereinander alle Dialogfenster mit der rechten Maustaste an, wieder die gleiche Folge: Zuerst links oben, dann rechts oben, dann links unten und zum Schluß rechts unten.
Dann sehen Sie folgendes Bild:
Alle Arbeitsthreads sind (getriggert durch
den Rechtsklick) in Richtung "Toilette" gestartet, aber nur der erste
(links
oben, den Sie zuerst angeklickt haben) Arbeitsthread hat es geschafft,
er hat natürlich sofort das Schloß verriegelt:
Nachdem er fünf Sekunden sein "Geschäft" (hier ::Sleep(5000) ) verrichtet hat, gibt er die "Toilette" wieder frei. Was passiert nun?
(Versuch mit Betriebssystem Windows 98 SE)
Das hängt davon ab, welches
Betriebssystem
Sie verwenden. Als Besitzer des altehrwürdigen Windows 98 stellen
Sie folgendes fest:
Da hat sich einer vorgedrängt! Der
Arbeitsthread, den Sie zuletzt gestartet haben, ist nun als Zweiter an
der Reihe. Dann kommt der links unten und zuletzt rechts oben.
Beschwerden
bitte an die Stewardess (äh, an das Betriebssystem). Es gilt bei
MS
Windows 98 offenbar das Prinzip: "Wer zuletzt kommt, darf als Erster."
Als Besitzer des neueren Windows XP entgeht Ihnen dieses merkwürdige Erlebnis. Alle Threads gehen geordnet nach ihrer Wartezeit auf die "Toilette".
Ich hoffe, die Analogie mit der
Flugzeugtoilette
hilft Ihnen bei der geistigen Visualisierung des Vorgangs.
In unserem Beispiel machen wir nichts
besonderes:
key.Lock();
::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Gesperrt");
::Sleep(5000);
key.Unlock();
Wir drehen den Schlüssel um und schlafen 5 Sekunden. Nicht sonderlich aufregend.
Stellen Sie sich nun mehrere
verschiedene
Anwendungen vor, die z.B. über eine Datei oder die Registry
kommunizieren
bzw. eine andere Ressource quasiparallel manipulieren. Da wird das
schon
spannender. Sie wissen nun wie es geht.
Wenn man verhindern will, dass eine
Anwendung
mehr als einmal gestartet wird, gibt es zunächst den klassischen
Weg
mit
HWND FindWindow( LPCTSTR
lpClassName,
LPCTSTR lpWindowName ). Nachfolgend ein einfaches
WinAPI-Beispiel:
if(
::FindWindow("GesuchterKlassenName","GesuchterFensterTitel")
) { ::PostQuitMessage(0); return 0; } |
Mit einem Mutex kann man nun eine
interessante
Alternative anwenden. Sie finden hier zur besseren Übersicht eine
MFC-Anwendung, die ohne Assistent und in einer einzigen Datei
programmiert
ist :
#include
<afxwin.h>
class
CFenster : public
CFrameWnd class
CMeinProgramm :
public CWinApp BOOL
CMeinProgramm ::
InitInstance() m_pMainWnd
= new
CFenster(); CMeinProgramm test; |
Zunächst erkennen Sie, dass wir in InitInstance() für den Mutex den direkten Weg über die WinAPI gewählt haben. Das erspart auch die Einbindung von afxmt.h (siehe unten).
Die WinAPI-Funktion zur Erzeugung eines Mutex lautet:
HANDLE CreateMutex
(
LPSECURITY_ATTRIBUTES
lpMutexAttributes, // pointer to security
attributes
BOOL
bInitialOwner,
// flag for initial ownership
LPCTSTR
lpName
// pointer to mutex-object name
);
Der Trick liegt hier darin begründet, dass wir beim Erstellen des Mutex den Parameter bInitialOwner auf TRUE gesetzt haben. Damit geht der Mutex sofort in den Besitz dieses Threads über. Wird die Anwendung erneut gestartet, dann stellt genau diese Funktion auch fest, dass der Mutex bereits existiert und liefert einen Handle auf den bereits existierenden Mutex zurück. Die Funktion GetLastError() resultiert dann den Wert ERROR_ALREADY_EXISTS. Diesen Zusammenhang nutzen wir hier auf direkte Weise aus.
Die Hülle, die die MFC-Klasse CMutex um die WinAPI legt, ist sehr dünn. Die Klassendefinition findet man in afxmt.h und die Implementierung in mtex.cpp. Wagen wir einen Blick hinein:
"MFC under the hood": CMutex
//Klassendefinition
in Datei afxmt.h class CMutex : public CSyncObject { DECLARE_DYNAMIC(CMutex) public: //Implementierung in Datei mtex.cpp CMutex::CMutex(BOOL
bInitiallyOwn,
LPCTSTR pstrName, LPSECURITY_ATTRIBUTES lpsaAttribute /*
= NULL */) CMutex::~CMutex(){} BOOL CMutex::Unlock(){ return ::ReleaseMutex(m_hObject);} |
Semaphore
Ein Semaphor (engl. Signalmast) ist ein Synchronisationsobjekt, dass es erlaubt, dass eine begrenzte Zahl von Threads in einem oder mehreren Prozessen auf eine gemeinsame Ressource geordnet zugreifen. Die MFC-Klasse CSemaphore behält die Übersicht über die Zahl zugreifender Threads und begrenzt die Nutzerzahl einer Ressource.
Der Konstruktor hat folgende Parameter:
CSemaphore
(
LONG
lInitialCount
= 1,
LONG
lMaxCount
= 1,
LPCTSTR
pstrName
= NULL,
LPSECURITY_ATTRIBUTES
lpsaAttributes
= NULL
);
Der InitialCount muß größer/gleich 0 und kleiner/gleich MaxCount sein. Normalerweise setzt man diesen Wert auf MaxCount, da diese Zahl abwärts auf 0 gezählt wird. MaxCount ist die größte zulässige Zahl an parallelen Zugriffen. lpszName ist der optionale Name für die Interprozesskommunikation (analog CMutex).
Ansonsten gibt es die bekannten Funktionen Lock() und Unlock().
Wir betrachten unser "Flugzeugtoiletten-Programm" aus dem Bereich CMutex und verändern es so, dass zwei Threads gleichzeitig auf die Ressource zugreifen dürfen.
Anstelle
#include
"afxmt.h" CMutex key( FALSE, "MeinSchluessel" ); |
nimmt man einfach
#include
"afxmt.h" CSemaphore key( 2, 2, "MeinSchluessel" ); |
Das ist wirklich alles!
Nehmen Sie ein etwas kleineres Dialogfeld (ohne OK und Abbruch), damit diese Versuche auch mit mehr als vier Prozessen möglich sind. Nachfolgend findet man ein Beispiel mit 8 gestarteten Prozessen. Die oberen beiden Arbeitsthreads sind bereits fertig mit ihrem Zugriff, die vier in der Mitte warten noch, und die zwei unteren (wieder die zuletzt gestarteten) sind gerade beim Zugriff, sozusagen Besitzer des Semaphors. Zumindest läuft das unter Windows 98 so ab. Bei Windows XP geht es neuerdings der Reihe nach. Das kennen Sie ja schon von dem Mutex-Beispiel.
Fassen wir an dieser Stelle kurz zusammen:
Zur Regelung des quasiparallelen
Zugriffs
von Threads auf gleiche Ressourcen benutzt man einen
Verriegelungs-Mechanismus.
Kritische Abschnitte sind die richtige
Lösung innerhalb eines Prozesses, während man zwischen
mehreren
Prozessen Mutexe verwendet.
Die Identifikation erfolgt bei Mutex und
Semaphor über die Festlegung eines Namens.
Will man mehreren Threads gleichzeitig
den Zugang zu einer Ressource erlauben, ersetzt man Mutexe
einfach
durch Semaphoren.
MFC stellt die Funktionen Lock() und
Unlock()
für Sperrung und Freigabe bereit.
CEvent
Während bei CCriticalSection, CMutex und CSemaphore die Visualisierung eines Verriegelungsmechanismus der richtige Vergleich ist, kommen wir mit den Ereignissen (Events) zu einem anderen Mechanismus der Verständigung zwischen Threads.
Ereignisse (Events) sind ein Alarmsystem zwischen mehreren Threads. Ereignisse sind vom Betriebssystem gepflegte Flags. Man nennt sie auch "Threadzünder".
Es gibt hier neben dem Konstruktor
CEvent
(
BOOL
bInitiallyOwn
= FALSE,
BOOL
bManualReset
= FALSE,
LPCTSTR
lpszName
= NULL,
LPSECURITY_ATTRIBUTES
lpsaAttribute
= NULL
);
Parameter:
bInitiallyOwn
FALSE: Signal gesetzt, TRUE: Signal nicht gesetzt.
bManualReset
FALSE: "Autoreset", TRUE: manueller Reset.
lpszName
Name für die Interprozesskommunikation (analog CMutex und
CSemaphore)
noch die Funktionen:
SetEvent()
Hiermit wird das "Ereignis" gesetzt /
sinalisiert. Dies gibt alle wartenden Threads frei. Bei Ereignissen
unterscheidet
man einen "manuellen" und einen automatischen Reset. Beim manuellen
Reset
bleibt das Ereignis signalisiert, bis ResetEvent() diesen Signalzustand
beendet. Beim automatischen Reset wird der Signalzustand beendet,
sobald
der erste (!) Thread freigegeben wird. Weitere wartende Threads werden
nicht berücksichtigt.
ResetEvent()
Hiermit wird das "Ereignis"
zurückgesetzt
/ der Signalzustand beendet.
PulseEvent()
Hiermit wird das "Ereignis" gesetzt /
sinalisiert. Alle (!) wartenden Threads werden freigegeben und
anschließend
der Signalzustand automatisch - also ohne ResetEvent() -
zurückgesetzt.
Zwei oder mehr wartende Threads lassen sich nur über diese
Funktion
gemeinsam "entsperren" und damit fortsetzen.
Lock()
Ein CEvent Objekt wird aktiviert und
sperrt
damit den aufrufenden Thread. Dieser wartet dann darauf, das ein
anderer
Thread dieses Ereignis mit SetEvent() oder PulseEvent() signalisiert.
Unlock()
Das CEvent Objekt wird freigegeben.
Klingt alles recht kompliziert. Da helfen nur praktische Beispiele.
Nehmen Sie zunächst unser kleines
Übungsbeispiel von CMutex und CSemaphore mit folgender
Veränderung:
#include "afxmt.h" CEvent alarm( FALSE, FALSE, "MeinSignal" ); UINT thrFunction1(LPVOID
pParam)
alarm.SetEvent();
::SetDlgItemText(HWND(pParam),IDC_EDIT1,"Gesperrt"); return 0;
|
Lassen Sie sich Zeit im Verständnis. Betrachten wir zunächst eine einzelne Anwendung:
Nach dem Rechtsklick startet der Arbeitsthread thrFunction1(...), und das Edit-Feld zeigt "Gestartet".
3 Sekunden später wird das
(prozessübergreifende)
Autoreset-Ereignis "MeinSignal" gesetzt/signalisiert.
Das Edit-Feld zeigt "Signal gesetzt".
Wieder 3 Sekunden später wird der
Thread durch alarm.lock() angehalten.
Da das Ereignis noch signalisiert ist,
erhält der Thread sofort die Freigabe.
Der Thread läuft weiter, und Sie
sehen "Wieder offen".
Interessanter wird es mit zwei gestarteten Anwendungen.
Sie klicken zuerst die linke, dann die rechte Anwendung an. Hier schafft es nur der zuerst gestartete Arbeitsthread das alarm.lock() zu überwinden. Er setzt das (für beide Threads gültige) Ereignis zurück, und für den zu spät kommenden Thread gibt es nur die verschlossene Tür (das zurückgesetzte/nicht-signalisierte Ereignis).
Der Grund hierfür ist der im Konstruktor angegebene "Autoreset":
CEvent alarm( FALSE, FALSE, "MeinSignal" );
Wenn Sie diesen Parameter auf TRUE
setzen,
muß "manuell", d.h. im Programm, die Funktion ResetEvent()
eingesetzt
werden.
Probieren Sie es aus.
Ein anderer Versuch:
Ersetzen Sie SetEvent() durch PulseEvent(),
um dann Experimente mit mehreren gestarteten Anwendungen
durchzuführen.
Diese vielfältigen Möglichkeiten lassen sich hier nicht optimal visualisieren. Hier geht "Probieren über Studieren".
Entscheidend ist, dass Sie verstehen,
dass
man Threads mit Lock() an einem Ereignis anhält. Wenn das Ereignis
signalisiert ist ("grüne Ampel"), dann geht die Fahrt weiter,
falls
nicht ("rote Ampel"), muß erst ein anderer Thread das Ereignis
signalisieren
("Ampel auf grün stellen").
Mit SetEvent() gibt man einem wartenden
Thread den Weg frei. Mehrere wartende Threads benötigen ein
PulseEvent().
Bei PulseEvent() darf das Autoreset-Flag jedoch nicht gesetzt sein. Um
das ResetEvent() kümmert sich PulseEvent() nach der Fortsetzung
aller
wartenden Threads selbst.
Deadlock
Stellen Sie sich vor, Sie erstellen ein Simulationsprogramm für eine Verkehrskreuzung mit Rechts-vor-Links-Vorfahrt. Solange das Verkehrsaufkommen niedrig ist, klappt das sicher gut. Wenn jedoch plötzlich bei ansteigendem Verkehr von jeder Seite der Kreuzung ein Fahrzeug kommt, entsteht die typische "Deadlock"-Situation. Kein Fahrzeug darf fahren, weil ein anderer ihm gegenüber die Vorfahrt hat.
Dies kann auch bei Multithreading-Programmen bei entsprechenden Rahmenbedingungen passieren. Kein Arbeitsthread kann mehr weiter. Das Programm steckt in diesem Fall sozusagen am toten Punkt (engl. deadlock) fest. Wenn diese Möglichkeit besteht, müssen Sie die "Regeln" entsprechend verändern, dass diese Situation vermieden wird.
Weitere Beispiele für solche Situationen können Sie z.B. hier sehen:
Dining Philosophers:
http://www.hta-be.bfh.ch/~fischli/kurse/threads/
http://www.hta-be.bfh.ch/~fischli/kurse/threads/phil/index.html
http://www-dse.doc.ic.ac.uk/concurrency/
Producer/Consumer Problem:
http://www.hta-be.bfh.ch/~fischli/kurse/threads/prodcons/
http://www.cs.mtu.edu/~shene/NSF-3/e-Book/MONITOR/ProducerConsumer-1/MON-example-buffer-1.html
http://www.cs.mtu.edu/~shene/NSF-3/e-Book/SEMA/VISUAL/VISUAL-sema-buffer.html
http://cne.gmu.edu/modules/ipc/aqua/producer.html
Sleeping Barber Problem /
Barbershop
Problem:
http://www.cis.ksu.edu/saves/eap/examples/barber/prbE.html
http://cs.millersville.edu/~webster/cs380/assignment4.html
http://www.math.grin.edu/~walker/courses/213.fa00/lab-barbershop.html
Reader/Writer Problem:
http://www.hta-be.bfh.ch/~fischli/kurse/threads/readwrit/index.html
http://cne.gmu.edu/modules/ipc/aqua/readers.html
http://cne.gmu.edu/modules/ipc/orange/readsem.html
http://www.mrs.umn.edu/~swl/cs3400/fall98/ipc.html#readerwriter
Cigarette Smoker Problem:
http://www.cs.umd.edu/~hollings/cs412/s96/synch/smokers.html
http://www.cs.cmu.edu/~emc/15-398/assignments/parnas_smokers.pdf
http://webster.cs.uga.edu/~maria/classes/4730-Spring-2002/project3/project3.txt
Monkey Rock Problem (und alle anderen):
http://i30www.ira.uka.de/teaching/coursedocuments/39/sysarch-7-thread051_sel.pdf
(Links vom Stand Aug. 2002)