Manchmal ist es angebracht, Anwendungen mehrere Dinge gleichzeitig ausführen zu lassen. Die Anwendung könnte eine Sicherungsdatei schreiben oder im Hintergrund drucken, während der Benutzer am selben Dokument weiterarbeitet. Vielleicht führt Ihre Anwendung Berechnungen aus, während der Benutzer neue Daten eingibt, oder zeichnet mehrere Bilder gleichzeitig. Es gibt viele verschiedene Gründe, warum man diese Fähigkeit, das sogenannte Multitasking, in Anwendungen realisieren möchte. Windows stellt verschiedene Einrichtungen bereit, um derartige Fähigkeiten in eine Anwendung einbauen zu können.
In den Tagen von Windows 3.x waren alle Windows-Anwendungen Singlethreading- Anwendungen, die zu einem beliebigen Zeitpunkt nur einen Ausführungspfad aufwiesen. Die in Windows 3.x realisierte Form des Multitasking bezeichnet man als kooperatives Multitasking. Der Schlüssel zu dieser Art des Multitasking liegt darin, daß jede einzelne Anwendung darüber entscheidet, wann sie den Prozessor für eine andere Anwendung freigibt, damit diese die anstehenden Arbeiten erledigen kann. Im Ergebnis war Windows 3.x anfällig für eine sich schlecht verhaltende Anwendung, die andere Anwendungen gefangenhält, während sie einen länger dauernden Prozeß ausführt oder sich sogar selbst in irgendeiner Schleife verfängt.
Mit Windows NT und Windows 95 hat sich die Natur des Betriebssystems geändert. Das neue präemptive Multitasking hat das kooperative Multitasking abgelöst. Beim präemptiven Multitasking entscheidet das Betriebssystem, wann es der einen Anwendung den Prozessor entzieht und einer anderen Anwendung, die bereits darauf wartet, zuteilt. Es spielt keine Rolle, ob die Anwendung, die momentan den Prozessor für sich in Anspruch nimmt, bereit ist, den Prozessor abzugeben. Das Betriebssystem entzieht der Anwendung den Prozessor, ohne die Anwendung dazu um Erlaubnis zu fragen. Auf diese Weise trägt das Betriebssystem dazu bei, daß mehrere Anwendungen rechenintensive Aufgaben erledigen können und trotzdem alle Anwendungen in ihren eigenen Verarbeitungen vorwärts kommen. Indem man diese Fähigkeit dem Betriebssystem überträgt, verhindert man, daß eine einzige Anwendung alle anderen Anwendungen blockiert, während sie den Prozessor beansprucht.
Zusätzlich zur Fähigkeit, mehrere Anwendungen simultan ausführen zu können, kommt die Fähigkeit, daß eine einzelne Anwendung mehrere Threads zu ein und demselben Zeitpunkt ausführt. Ein Thread (Programmfaden) ist für eine Anwendung das, was eine Anwendung für das Betriebssystem darstellt. Wenn eine Anwendung mehrere Threads laufen hat, führt sie im Prinzip mehrere Anwendungen innerhalb der Anwendung als Ganzes aus und kann damit mehrere Dinge gleichzeitig erledigen. Beispielsweise prüft Microsoft Word die Rechtschreibung zum gleichen Zeitpunkt, zu dem man Eingaben in das Dokument vornimmt.
Einer der einfachsten Wege, um die Anwendung mehrere Verarbeitungen gleichzeitig ausführen zu lassen, ist die Realisierung der Leerlaufverarbeitung. Eine Leerlaufverarbeitung kommt zum Zuge, wenn sich eine Anwendung im Leerlaufzustand befindet. Im wahrsten Sinne des Wortes wird eine Funktion in der Anwendungsklasse aufgerufen, wenn keine Nachrichten in der Nachrichtenwarteschlange der Anwendung stehen. Der Gedanke hinter dieser Funktion ist der, daß eine Anwendung im Leerlaufzustand Aufgaben wie Speicherbereinigung (die sogenannte Garbage Collection) oder Schreiben in einen Drucker-Spooler ausführen kann.
Die Funktion OnIdle
ist ein Überbleibsel aus den Tagen von Windows 3.x. Sie gehört
zur Klasse CWinApp
, von der Ihre Anwendung abgeleitet ist. Per Vorgabe fügt der Anwendungs-Assistent
keinerlei Verarbeitungen in diese Funktion ein. Wenn Sie demzufolge
die Funktion in Ihrer Anwendung nutzen möchten, müssen Sie sie in Ihre Anwendungsklasse
mit dem Klassen-Assistenten aufnehmen. (OnIdle
ist eine der
verfügbaren Nachrichten für die Anwendungsklasse in Ihren Anwendungen.)
Die Funktion OnIdle
übernimmt ein Argument, das die Anzahl der Aufrufe dieser
Funktion angibt, die seit der letzten von der Anwendung verarbeiteten Nachricht stattgefunden
haben. Anhand dieses Wertes können Sie ermitteln, wie lange die Anwendung
im Leerlauf gearbeitet hat und wann eine Aktion auszulösen ist, falls die Anwendung
eine bestimmte Zeitbegrenzung im Leerlauf überschritten hat.
Einer der wichtigsten Punkte bei der OnIdle
-Verarbeitung in Ihren Anwendungen ist
der, daß der Umfang der Funktionalität klein sein und die Steuerung schnell an den
Benutzer zurückgehen muß. Wenn eine Anwendung eine OnIdle
-Verarbeitung durchführt,
kann der Benutzer nicht mit der Anwendung weiterarbeiten, bis die OnIdle
-Verarbeitung
abgeschlossen ist und der Benutzer die Steuerung wiedererlangt. Wenn Sie
eine längere, ausgedehnte Aufgabe in der Funktion OnIdle
ausführen müssen, sollten
Sie sie in viele kleine und schnelle Fragmente aufteilen, damit der Benutzer die Steuerung
zwischenzeitlich zurückerhält. Sobald die Nachrichtenwarteschlange wieder leer
ist, können Sie Ihre OnIdle
-Verarbeitung fortsetzen. Das bedeutet, daß Sie auch den
Fortschritt der Anwendung in der OnIdle
-Verarbeitung verfolgen müssen, damit die
Anwendung beim nächsten Aufruf der Funktion OnIdle
die Verarbeitung an der Stelle
wieder aufnehmen kann, wo sie verlassen wurde.
Wenn Sie tatsächlich eine längere Hintergrundverarbeitung ausführen müssen, die der Benutzer nicht unterbrechen soll, müssen Sie einen unabhängigen Thread erzeugen. Ein Thread verhält sich wie eine andere Anwendung, die innerhalb Ihrer Anwendung läuft. Der Thread muß nicht darauf warten, daß die Anwendung in den Leerlauf gelangt, um seine Aufgaben auszuführen, und er bewirkt auch nicht, daß der Benutzer warten muß, bis der Thread die Steuerung zwischenzeitlich abgibt.
Die beiden Methoden zum Erzeugen eines unabhängigen Threads verwenden die gleiche
Funktion, um den Thread zu erzeugen und zu starten. Um einen unabhängigen
Thread zu erzeugen und zu starten, ruft man die Funktion AfxBeginThread
auf. Dabei
kann man wählen, ob man ihr eine aufzurufende Funktion für die Ausführung der
Thread-Verarbeitung oder einen Zeiger auf die Laufzeitklasse eines Objekts, das von
der Klasse CWinThread
abgeleitet ist, übergibt. Beide Versionen der Funktion liefern
einen Zeiger auf ein CWinThread
-Objekt zurück, das als unabhängiger Thread läuft.
In der ersten Version der Funktion AfxBeginThread
ist das erste Argument ein Zeiger
auf die Hauptfunktion, um den Thread zu starten. Diese Funktion ist das Äquivalent
der Funktion main
in einem C/C++-Programm. Sie steuert die Ausführung auf höchster
Ebene für den Thread. Diese Funktion ist als UINT
-Funktion mit einem einzelnen
LPVOID
-Argument zu definieren:
UINT MyThreadFunction( LPVOID pParam);
Diese Version von AfxBeginThread
erfordert auch ein zweites Argument, das als einziges
Argument an die Hauptfunktion des Threads weitergereicht wird. Dieses Argument
kann ein Zeiger auf eine Struktur sein, die alle Informationen enthält, die der
Thread für die korrekte Ausführung seiner Aufgabe kennen muß.
Das erste Argument an die zweite Version der Funktion AfxBeginThread
ist ein Zeiger
auf die Laufzeitklasse eines Objekts, das von der Klasse CWinThread
abgeleitet ist.
Einen Zeiger auf die Laufzeitklasse Ihrer CWinThread
-Klasse können Sie mit dem Makro
RUNTIME_CLASS
ermitteln, wobei Sie Ihre Klasse als einziges Argument übergeben.
Nach diesen ersten Argumenten sind die restlichen Argumente an die Funktion AfxBeginThread
bei beiden Versionen gleich und insgesamt optional. Das erste dieser
Argumente bezeichnet die gewünschte Priorität des Threads, wobei der Wert
THREAD_PRIORITY_NORMAL
vorgegeben ist. Tabelle 18.1 listet die verfügbaren Thread-
Prioritäten auf.
Das nächste Argument an die Funktion AfxBeginThread
ist die Stack-Größe, die für
den neuen Thread bereitzustellen ist. Der Standardwert für dieses Argument ist 0. Damit
erhält der Thread die gleiche Stack-Größe wie die Hauptanwendung.
Als nächstes Argument an die Funktion AfxBeginThread
übergibt man das Flag zum
Erzeugen des Threads. Dieses Flag kann einen von zwei Werten enthalten. Es steuert,
wie der Thread gestartet wird. Wenn man für dieses Argument den Wert
CREATE_SUSPENDED
übergibt, wird der Thread in einem angehaltenen Zustand erzeugt.
Der Thread startet erst dann, wenn die Funktion ResumeThread
für den Thread aufgerufen
wird. Übergibt man für dieses Argument den Wert 0, d.h. den Standardwert, beginnt
der Thread die Ausführung in dem Moment, zu dem er erzeugt wird.
Das letzte Argument an die Funktion AfxBeginThread
ist ein Zeiger auf die Sicherheitsattribute
für den Thread. Der Standardwert für dieses Argument ist NULL
. Damit
wird der Thread mit dem gleichen Sicherheitsprofil wie die Anwendung erzeugt. Solange
Sie keine Anwendungen für Windows NT erstellen und einen Thread mit einem
bestimmten Sicherheitsprofil bereitstellen müssen, sollten Sie für dieses Argument immer
den Standardwert einsetzen.
Stellen Sie sich eine Anwendung vor, die zwei Threads ausführt, wobei jeder seinen eigenen Satz von Variablen zur gleichen Zeit abarbeitet. Stellen wir uns weiterhin vor, daß die Anwendung ein globales Objektfeld verwendet, um diese Variablen aufzunehmen. Wenn die Methode zur Reservierung und Größenänderung des Arrays die aktuelle Größe prüft und eine Position am Ende des Feldes anfügt, bauen Ihre beiden Threads vielleicht ein Feld auf, das etwa gemäß Abbildung 18.1 gefüllt ist. Hier sind die durch den ersten Thread gefüllten Positionen mit denjenigen vermischt, die der zweite Thread erzeugt hat. Die beiden Threads können dabei leicht durcheinanderkommen, wenn sie Werte aus dem Feld abrufen und weiterverarbeiten wollen, da jeder Thread mit hoher Wahrscheinlichkeit einen Wert abruft, der eigentlich zum anderen Thread gehört. Das führt dazu, daß die Threads auf den falschen Daten arbeiten und falsche Ergebnisse zurückliefern.
Abbildung 18.1:
Zwei Threads, die ein gemeinsames Datenfeld füllen
Wenn die Anwendung die Felder als lokal und nicht als global erstellt, kann sie den Zugriff auf die Felder nur auf den Thread einschränken, der das Feld erstellt. Im Beispiel von Abbildung 18.2 tritt keine Vermischung von Daten aus mehreren Threads auf. Wenn Sie mit dieser Lösung bei Feldern und anderen Speicherstrukturen arbeiten, kann jeder Thread seine Aufgaben erledigen und die Ergebnisse an den Client zurückgeben. Dabei ist sichergestellt, daß es sich um die richtigen Ergebnisse handelt, da die Berechnungen auf eigenständigen Daten ablaufen.
Abbildung 18.2:
Zwei Threads, die lokal angelegte Datenfelder füllen
Nicht alle Variablen lassen sich in einer lokalen Form bereitstellen, und es ist oft erforderlich, bestimmte Ressourcen zwischen allen Threads, die in Ihren Anwendungen laufen, gemeinsam zu nutzen. Diese gemeinsame Nutzung führt zu einem Problem bei Anwendungen mit mehreren Threads. Nehmen wir an, daß drei Threads mit einem einzigen Zähler arbeiten, der eindeutige Zahlen erzeugt. Da man nicht weiß, wann die Steuerung des Prozessors von einem Thread zum nächsten übergeht, kann die Anwendung doppelte »eindeutige« Werte erzeugen, wie es Abbildung 18.3 zeigt.
Abbildung 18.3:
Drei Threads, die ein und denselben Zähler gemeinsam nutzen
Wie man sieht, funktioniert diese gemeinsame Nutzung in einer Anwendung mit mehreren Threads nicht besonders gut. Man braucht eine Möglichkeit, um den Zugriff auf eine gemeinsam genutzte Ressource zu einem bestimmten Zeitpunkt auf nur einen Thread einzuschränken. In der Tat existieren vier Mechanismen, mit denen man den Zugriff auf gemeinsame Ressourcen begrenzen und die Verarbeitung zwischen Threads synchronisieren kann.
Diese Mechanismen arbeiten nach verschiedenen Methoden. Welche davon geeignet ist, hängt von den konkreten Umständen ab. Die vier Mechanismen heißen:
Ein kritischer Abschnitt ist ein Mechanismus, der den Zugriff auf eine bestimmte Ressource auf einen einzelnen Thread innerhalb einer Anwendung beschränkt. Ein Thread tritt in den kritischen Abschnitt ein, bevor er mit der angegebenen gemeinsam genutzten Ressource arbeiten muß, und verläßt den kritischen Abschnitt, nachdem der Zugriff auf die Ressource abgeschlossen ist. Wenn ein anderer Thread versucht, in den kritischen Abschnitt einzutreten, bevor der erste Thread den kritischen Abschnitt verlassen hat, wird der zweite Thread blockiert und erhält keine Prozessorzeit, bis der erste Thread den kritischen Abschnitt verläßt und damit dem zweiten Thread den Eintritt ermöglicht. Mit kritischen Abschnitten markiert man Codebereiche, die nur ein Thread zu einem bestimmten Zeitpunkt ausführen sollte. Damit verhindert man nicht, daß der Prozessor von diesem Thread zu einem anderen umschaltet. Es wird nur unterbunden, daß zwei oder mehr Threads in denselben Codeabschnitt eintreten.
Wenn Sie einen kritischen Abschnitt mit dem Zähler gemäß Abbildung 18.3 verwenden, können Sie erzwingen, daß jeder Thread in einen kritischen Abschnitt eintritt, bevor der Thread den aktuellen Wert des Zählers prüft. Wenn der Thread den kritischen Abschnitt erst dann verläßt, wenn er den Zähler inkrementiert und aktualisiert hat, ist garantiert, daß - unabhängig davon wie viele Threads ausgeführt werden und in welcher Reihenfolge die Ausführung erfolgt - wirklich eindeutige Zahlen generiert werden, wie es aus Abbildung 18.4 hervorgeht.
Abbildung 18.4:
Drei Threads, die denselben Zähler verwenden, wobei der Zähler durch einen kritischen Abschnitt geschützt ist.
Wenn Sie ein Objekt mit einem kritischen Abschnitt in Ihrer Anwendung benötigen,
erzeugen Sie eine Instanz der Klasse CCriticalSection
. Dieses Objekt enthält die beiden
Methoden Lock
und Unlock
, mit denen Sie die Steuerung des kritischen Abschnitts
an sich nehmen bzw. freigeben.
Mutexe arbeiten grundsätzlich in der gleichen Weise wie kritische Abschnitte. Allerdings setzt man Mutexe ein, wenn man Ressourcen zwischen mehreren Anwendungen gemeinsam nutzen will. Mit einem Mutex läßt sich garantieren, daß keine zwei Threads, die in einer beliebigen Anzahl von Anwendungen laufen, auf dieselbe Ressource zum selben Zeitpunkt zugreifen.
Aufgrund ihrer systemweiten Verfügbarkeit bringen Mutexe wesentlich mehr Overhead mit als kritische Abschnitte. Die Lebensdauer eines Mutexes endet nicht, wenn die Anwendung, die ihn erzeugt hat, beendet wird. Der Mutex kann weiterhin von anderen Anwendungen verwendet werden. Das Betriebssystem muß demnach verfolgen, welche Anwendungen einen Mutex verwenden, und muß den Mutex zerstören, sobald er nicht mehr benötigt wird. Im Gegensatz dazu bringen kritische Abschnitte nur einen geringen Verwaltungsaufwand mit sich, da sie nicht außerhalb der Anwendung existieren, die sie erzeugt und verwendet. Nachdem die Anwendung beendet wird, ist der kritische Abschnitt verschwunden.
Wenn Sie einen Mutex in Ihren Anwendungen verwenden müssen, erzeugen Sie eine
Instanz der Klasse CMutex
. Der Konstruktor der Klasse CMutex
weist drei verfügbare
Argumente auf. Das erste Argument ist ein boolescher Wert, der festlegt, ob der
Thread, der das CMutex
-Objekt erzeugt, der anfängliche Besitzer des Mutex ist. In diesem
Fall muß dieser Thread den Mutex freigeben, bevor irgendwelche anderen
Threads darauf zugreifen können.
Das zweite Argument stellt den Namen für den Mutex dar. Alle Anwendungen, die gemeinsam
auf den Mutex zurückgreifen müssen, können ihn nach diesem textuellen
Namen identifizieren. Das dritte und letzte Argument an den CMutex
-Konstruktor ist
ein Zeiger auf die Sicherheitsattribute für das Mutex-Objekt.
Übergibt man für diesen Zeiger den Wert NULL
, verwendet das Mutex-Objekt die
Sicherheitsattribute des Threads, der den Mutex erzeugt hat.
Nachdem Sie ein CMutex
-Objekt erzeugt haben, können Sie es mit den Elementfunktionen
Lock
und Unlock
sperren bzw. freigeben. Damit haben Sie die Möglichkeit, den
Zugriff auf gemeinsam genutzte Ressourcen zwischen mehreren Threads in mehreren
Anwendungen zu steuern.
Die Arbeitsweise von Semaphoren unterscheidet sich grundsätzlich von kritischen Abschnitten und Mutexen. Semaphoren setzt man bei Ressourcen ein, die nicht auf einen einzelnen Thread zu einem Zeitpunkt beschränkt sind - eine Ressource, die auf eine bestimmte Anzahl von Threads beschränkt ist. Vom Prinzip her ist ein Semaphor eine Art Zähler, den die Threads inkrementieren oder dekrementieren können. Der Trick bei Semaphoren besteht darin, daß sie nicht kleiner als Null werden können. Wenn demzufolge ein Thread versucht, einen Semaphor zu dekrementieren, der bereits Null ist, wird dieser Thread blockiert, bis ein anderer Thread den Semaphor inkrementiert.
Nehmen wir an, daß eine Warteschlange von mehreren Threads gefüllt wird und ein Thread die Elemente aus der Warteschlange entfernt und weiterverarbeitet. Wenn die Warteschlange leer ist, hat der Thread, der Elemente entfernt und weiterverarbeitet, nichts zu tun. Dieser Thread kann in eine Leerlaufschleife eintreten, in der die Warteschlange ständig daraufhin untersucht wird, ob sich irgend etwas darin befindet. Das Problem bei diesem Szenario besteht darin, daß der Thread Verarbeitungszeit verbraucht, um eigentlich überhaupt nichts zu tun. Diese Prozessorzeiten könnte man für andere Threads vergeben, die wirklich etwas ausführen müssen. Wenn Sie eine Warteschlange mit Semaphoren steuern, kann jeder Thread, der Elemente in die Warteschlange stellt, den Semaphor für jedes plazierte Element inkrementieren, und der Thread, der die Elemente aus der Warteschlange entfernt, kann den Semaphor dekrementieren, bevor er ein Element aus der Warteschlange nimmt. Wenn die Warteschlange leer ist, hat der Semaphor den Wert Null, und der Thread, der Elemente entfernt, wird beim Aufruf zum Dekrementieren der Warteschlange blockiert. Der Thread verbraucht damit keine Prozessorzeit, bis einer der anderen Threads den Semaphor inkrementiert, um anzuzeigen, daß er ein Element in die Warteschlange gestellt hat. Zu diesem Zeitpunkt wird die Blockierung für den Thread, der Elemente entfernt, unverzüglich aufgehoben, und der Thread kann das eben plazierte Element aus der Warteschlange nehmen und weiterverarbeiten, wie es Abbildung 18.5 zeigt.
Abbildung 18.5:
Mehrere Threads, die Objekte in eine Warteschlange stellen
Wenn Sie einen Semaphor in Ihrer Anwendung einsetzen müssen, können Sie eine
Instanz der Klasse CSemaphore
erzeugen. Diese Klasse hat vier Argumente, die man
an den Konstruktor der Klasse übergeben kann. Das erste Argument gibt den anfänglichen
Zählerstand für den Semaphor an. Im zweiten Argument legt man den maximalen
Zählerstand für den Semaphor fest. Mit diesen beiden Argumenten läßt sich steuern,
wie viele Threads und Prozesse gleichzeitig auf eine gemeinsam genutzte
Ressource zugreifen können. Das dritte Argument spezifiziert den Namen für den
Semaphor. Dieser Name identifiziert den Semaphor gegenüber allen Anwendungen,
die im System laufen, analog zur Klasse CMutex
. Das letzte Argument ist ein Zeiger auf
die Sicherheitsattribute für den Semaphor.
Beim CSemaphore
-Objekt kann man mit den Elementfunktionen Lock
und Unlock
die
Steuerung des Semaphors erlangen oder freigeben. Ruft man die Funktion Lock
auf,
wenn der Verwendungszähler des Semaphors größer als Null ist, wird der Verwendungszähler
dekrementiert, und Ihr Programm kann weiterlaufen. Ist der Verwendungszähler
bereits Null, dann wartet die Funktion Lock
, bis der Verwendungszähler
inkrementiert wird, damit Ihr Prozeß den Zugriff auf die gemeinsam genutzte Ressource
erlangen kann. Wenn Sie die Funktion Unlock
aufrufen, wird der Verwendungszähler
des Semaphors inkrementiert.
In dem Maße wie die Synchronisierungsmechanismen für Threads dafür ausgelegt sind, den Zugriff auf begrenzte Ressourcen zu steuern, sind sie auch dafür vorgesehen, unnötige Prozessorzeit in Threads zu unterbinden. Je mehr Threads gleichzeitig laufen, desto langsamer führt jeder einzelne Thread seine Aufgaben aus. Wenn ein Thread also nichts zu tun hat, blockieren Sie ihn, und belassen Sie ihn im Leerlauf. Damit erhalten andere Threads mehr Prozessorzeit und laufen deshalb schneller, bis die Bedingungen erfüllt sind, daß der im Leerlauf arbeitende Thread etwas zu tun bekommt.
Aus diesem Grund verwendet man Ereignisse - um Threads den Leerlauf zu ermöglichen, bis die Bedingungen erfüllt sind, daß sie etwas zu tun bekommen. Ereignisse sind dem Namen nach mit den Ereignissen verwandet, die die meisten Windows-Anwendungen treiben, nur daß das Ganze etwas komplizierter ist. Die Ereignisse zur Synchronisierung von Threads arbeiten nicht nach den Mechanismen der Ereigniswarteschlangen und -behandlung. Statt eine Nummer zu erhalten und dann darauf zu warten, daß diese Nummer an die Behandlungsroutine von Windows übergeben wird, sind Ereignisse der Thread-Synchronisierung eigentliche Objekte, die sich im Speicher befinden. Jeder Thread, der auf ein Ereignis warten muß, teilt dem Ereignis mit, daß er auf dessen Auslösung wartet, und nimmt dann einen Ruhezustand ein. Wird das Ereignis ausgelöst, geht ein Signal an jeden Thread, der dem Ereignis seinen Wartezustand bekanntgegeben hat. Die Threads nehmen die Verarbeitung genau an dem Punkt auf, wo sie dem Ereignis die Warteabsicht mitgeteilt haben.
Wenn Sie in Ihrer Anwendung mit Ereignissen arbeiten, können Sie ein CEvent
-Objekt
einsetzen. Das CEvent
-Objekt ist zu erzeugen, wenn Sie auf das Ereignis zugreifen
und darauf warten müssen. Sobald der CEvent
-Konstruktor zurückkehrt, ist das Ereignis
aufgetreten, und Ihr Thread kann seine Arbeit weiter fortsetzen.
Der Konstruktor für die CEvent
-Klasse kann vier Argumente übernehmen. Das erste
Argument ist ein boolesches Flag. Es gibt an, ob der Thread das von ihm erzeugte
Ereignis anfänglich besitzt. Diesen Wert sollte man auf TRUE
setzen, wenn der das
CEvent
-Objekt erzeugende Thread auch ermittelt, wann das Ereignis auftritt.
Das zweite Argument an den CEvent
-Konstruktor legt fest, ob es sich um ein automatisches
oder manuelles Ereignis handelt. Ein manuelles Ereignis verbleibt im signalisierten
oder nicht signalisierten Zustand, bis es der Thread, der das Ereignisobjekt besitzt,
explizit in den anderen Zustand versetzt. Ein automatisches Ereignis verbleibt die
meiste Zeit im nicht signalisierten Zustand. Wurde es in den signalisierten Zustand gesetzt
und mindestens ein Thread wird freigegeben und setzt seine Arbeit im Ausführungspfad
fort, kehrt das Ereignis in den nicht signalisierten Zustand zurück.
Das dritte Argument an den Ereigniskonstruktor ist der Name für das Ereignis. Alle Threads, die auf das Ereignis zugreifen müssen, identifizieren es anhand dieses Namens. Das vierte und letzte Argument ist ein Zeiger auf die Sicherheitsattribute für das Ereignisobjekt.
Die Klasse CEvent
verfügt über mehrere Member-Funktionen, mit denen Sie den Zustand
des Ereignisses steuern können. Tabelle 18.2 listet diese Funktionen auf.
Versetzt das Ereignis in den signalisierten Zustand und dann wieder zurück in den nicht signalisierten. | |
Um zu zeigen, wie Sie eigene Multitasking-Anwendungen erstellen können, erzeugen
Sie eine Anwendung mit vier sich drehenden Farbfächern, von denen jeder in seinem
eigenen Thread läuft. Zwei der Fächer verwenden die Funktion OnIdle
, während die
beiden anderen als unabhängige Threads laufen. Mit dieser Einrichtung können Sie
die Unterschiede zwischen beiden Typen des Threadings studieren und lernen, wie
man jeden Typ einsetzt. Das Anwendungsfenster erhält vier Kontrollkästchen, um jeden
Thread zu starten und anzuhalten. Auf diese Weise läßt sich demonstrieren, wieviel
Verarbeitungsleistung das System erbringen muß, wenn jeder Thread allein oder
in Verbindung mit den anderen läuft.
Für die heute zu erstellende Anwendung ist das Gerüst einer SDI-Anwendung erforderlich,
bei dem die Ansichtsklasse von der Klasse CFormView
abgeleitet ist, so daß Sie
mit dem Dialog-Editor das Layout der wenigen Steuerelemente im Fenster gestalten
können. In der Dokumentklasse sind die Fächer und die unabhängigen Threads realisiert.
Die Ansicht enthält die Kontrollkästchen und Variablen, die Aktivität oder Leerlauf
der Threads steuern.
Erzeugen Sie mit dem MFC-Anwendungs-Assistenten einen neuen Projekt-Arbeitsbereich.
Geben Sie der Anwendung einen passenden Projektnamen wie etwa Tasking
.
Im Anwendungs-Assistenten wählen Sie die Option Einzelnes Dokument
(SDI). Die
Vorgaben der meisten anderen Dialogfelder des Anwendungs-Assistenten können Sie
übernehmen, obwohl eigentlich keine Unterstützung für ActiveX-Steuerelemente, andockbare
Symbolleisten, Statusleiste zu Beginn sowie Drucken und Seitenansicht erforderlich
ist. Schalten Sie die betreffenden Kontrollkästchen einfach aus. Im letzten
Dialogfeld des Assistenten legen Sie für Ihre Ansichtsklasse (CTaskingView
) die Basisklasse
CFormView
fest.
Im fertigen Anwendungsgerüst löschen Sie das ZU ERLEDIGEN
-Textfeld aus dem
Hauptfenster der Anwendung und fügen vier Kontrollkästchen etwa in der oberen linken
Ecke jedes Quadranten hinzu, wie es Abbildung 18.6 zeigt. Legen Sie die Eigenschaften
der Kontrollkästchen gemäß Tabelle 18.3 fest.
Abbildung 18.6:
Der Entwurf des Hauptfensters
Nachdem Sie die Kontrollkästchen in das Fenster aufgenommen und die Eigenschaften konfiguriert haben, fügen Sie mit dem Klassen-Assistenten für jedes Kontrollkästchen eine Variable hinzu. Namen und Datentyp der Variablen sind in Tabelle 18.4 zusammengefaßt.
Bevor Sie Threads in Ihre Anwendung hinzufügen können, erstellen Sie den drehenden Farbfächer, auf dem die Threads operieren. Da sich alle vier Farbfächer unabhängig voneinander drehen, ist es sinnvoll, die gesamte Funktionalität in einer einzigen Klasse zu verkapseln. Diese Klasse verfolgt, welche Farbe gezeichnet wird, wo im Fächer die nächste Linie zu zeichnen ist, welche Größe der Fächer hat und wo sich der Fächer im Anwendungsfenster befindet. Außerdem braucht die Klasse einen Zeiger auf die Ansichtsklasse, um den Gerätekontext zu holen und sich selbst darin zu zeichnen. Für die unabhängigen Fächer benötigt die Klasse einen Zeiger auf das Flag, das steuert, ob sich der Fächer drehen soll.
Für die Fächerklasse erzeugen Sie eine neue allgemeine Klasse, die von der Basisklasse
CObject
abgeleitet ist. Das Beispiel verwendet für den Namen der neuen Klasse
CSpinner
.
Der neu erstellten Klasse für Ihr Fächerobjekt fügen Sie nun einige Variablen hinzu. Gemäß der objektorientierten Richtlinien legen Sie diese Variablen als »Privat« fest und nehmen Methoden in die Klasse auf, um die Werte der Variablen zu setzen und abzurufen.
Die Variablen sind für die folgenden Werte vorgesehen:
Tabelle 18.5 gibt die Namen und Datentypen dieser Variablen an, die Sie alle in die Fächerklasse einfügen.
Die Farbtabelle mit allen Farben, die im Farbfächer zu zeichnen sind. | ||
Ein Zeiger auf die Variable des Kontrollkästchens, die festlegt, ob dieser Thread laufen soll. |
Wenn Sie alle erforderlichen Variablen hinzugefügt haben, müssen Sie sicherstellen,
daß Ihre Klasse die Variablen entweder initialisiert oder ein geeignetes Instrument bereitstellt,
um die Werte der Variablen zu setzen und abzurufen. Die Integer-Variablen
können den Anfangswert 0 erhalten. Die Zeiger sollten Sie mit NULL
initialisieren. Die
Initialisierungen lassen sich insgesamt im Konstruktor der Klasse erledigen, wie es
Listing 18.1 zeigt.
Listing 18.1: Der Konstruktor von CSpinner
1: CSpinner::CSpinner()
2: {
3: // Position, Größe und Farbe initialisieren
4: m_iRadius = 0;
5: m_nMinute = 0;
6: m_crColor = 0;
7: // Zeiger auf NULL setzen
8: m_pViewWnd = NULL;
9: m_bContinue = NULL;
10: }
Für diejenigen Variablen, die Sie setzen und abrufen müssen, ist Ihre Fächerklasse einfach
genug, daß Sie alle diesbezüglichen Funktionen als Inline-Funktionen in der Deklaration
der Klasse schreiben können. Farbe und Position werden automatisch durch
das Fächerobjekt berechnet, so daß für diese beiden Variablen keine Funktionen für
das Setzen erforderlich sind. Die übrigen Variablen (nicht mitgezählt die Farbtabelle)
müssen sich aber setzen lassen. Die einzigen Variablen, die Sie aus dem Fächerobjekt
abrufen müssen, sind die Zeiger auf die Ansichtsklasse und die Variable des Kontrollkästchens.
Die betreffenden Funktionen können Sie in die Deklaration der Klasse
CSpinner
aufnehmen, indem Sie die Header-Datei öffnen und die Inline-Funktionen
gemäß Listing 18.2 einfügen.
Listing 18.2: Die Klassendeklaration von CSpinner
1: class CSpinner : public CObject
2: {
3: public:
4: BOOL* GetContinue() {return m_bContinue;}
5: void SetContinue(BOOL* bContinue) { m_bContinue = bContinue;}
6: CWnd* GetViewWnd() { return m_pViewWnd;}
7: void SetViewWnd(CWnd* pWnd) { m_pViewWnd = pWnd;}
8: void SetLength(int iLength) { m_iRadius = iLength;}
9: void SetPoint(CPoint pPoint) { m_pCenter = pPoint;}
10: CSpinner();
11: virtual ~CSpinner();
12:
13: private:
14: BOOL* m_bContinue;
15: CWnd* m_pViewWnd;
16: static COLORREF m_crColors[8];
17: int m_iRadius;
18: CPoint m_pCenter;
19: int m_nMinute;
20: int m_crColor;
21: };
Nachdem Sie nun alle Unterstützungsfunktionen für das Setzen und Abrufen der erforderlichen Variablen hinzugefügt haben, müssen Sie die Farbtabelle deklarieren und füllen. Das entspricht in etwa der Definition für die Farbtabelle, die Sie in die Anwendung von Tag 10 aufgenommen haben. Die Farbtabelle besteht aus acht RGB- Werten, wobei jeder Wert entweder 0 oder 255 sein kann und jede Kombination dieser beiden Einstellungen möglich ist. Der beste Platz für diese Tabellendeklaration ist in der Quellcodedatei des Fächers unmittelbar vor dem Konstruktor der Klasse, wie es Listing 18.3 angibt.
Listing 18.3: Die Farbtabelle von CSpinner
1: static char THIS_FILE[]=__FILE__;
2: #define new DEBUG_NEW
3: #endif
4:
5: COLORREF CSpinner::m_crColors[8] = {
6: RGB( 0, 0, 0), // Schwarz
7: RGB( 0, 0, 255), // Blau
8: RGB( 0, 255, 0), // Grün
9: RGB( 0, 255, 255), // Cyan
10: RGB( 255, 0, 0), // Rot
11: RGB( 255, 0, 255), // Magenta
12: RGB( 255, 255, 0), // Gelb
13: RGB( 255, 255, 255) // Weiß
14: };
15:
16: //////////////////////////////////////////////////////////////////////
17: // Konstruktion/Destruktion
18: //////////////////////////////////////////////////////////////////////
19:
20: CSpinner::CSpinner()
21: {
22: // Position, Größe und Farbe initialisieren
23: m_iRadius = 0;
24: .
25: .
26: .
Nun kommt der angenehme Teil: das Fächerobjekt tatsächlich in Drehung versetzen. Um dies zu erreichen, berechnen Sie die neue Position für den Start- und Endpunkt jeder Linie, setzen den Ursprungspunkt des Viewports, wählen die Zeichenfarbe und erzeugen einen Zeichenstift in dieser Farbe. Anschließend zeichnen Sie die Linie vom Startpunkt zum Endpunkt. Danach stellen Sie den ursprünglichen Stift - der vor dem Zeichnen der Linie gültig war - wieder her. Vor dem Verlassen der Funktion berechnen Sie noch die Position der nächsten Linie.
Um diese Funktionalität in das Fächerobjekt aufzunehmen, fügen Sie eine Member-
Funktion in die Klasse CSpinner
ein. Legen Sie den Typ als void
, den Namen mit
Draw
und den Zugriff als Public
fest. In die Funktion schreiben Sie den Code aus
Listing 18.4.
Listing 18.4: Die Funktion Draw der Klasse CSpinner
1: void CSpinner::Draw()
2: {
3: // Zeiger auf Gerätekontext holen
4: CDC *pDC = m_pViewWnd->GetDC();
5: // Abbildungsmodus festlegen
6: pDC->SetMapMode (MM_LOENGLISH);
7: // Mittelpunkt des Fächers kopieren
8: CPoint org = m_pCenter;
9: CPoint pStartPoint;
10: // Anfangspunkt festlegen
11: pStartPoint.x = (m_iRadius / 2);
12: pStartPoint.y = (m_iRadius / 2);
13: // Ursprung festlegen
14: org.x = m_pCenter.x + (m_iRadius / 2);
15: org.y = m_pCenter.y + m_iRadius;
16: // Ursprung des View-Ports festlegen
17: pDC->SetViewportOrg(org.x, org.y);
18:
19: CPoint pEndPoint;
20: // Winkel zur nächsten Linie berechnen
21: double nRadians = (double) (m_nMinute * 6) * 0.017453292;
22: // Endpunkt der Linie festlegen
23: pEndPoint.x = (int) (m_iRadius * sin(nRadians));
24: pEndPoint.y = (int) (m_iRadius * cos(nRadians));
25:
26:
27: // Zeichenstift erzeugen
28: CPen pen(PS_SOLID, 0, m_crColors[m_crColor]);
29: // Stift auswählen
30: CPen* pOldPen = pDC->SelectObject(&pen);
31:
32: // Zum Anfangspunkt gehen
33: pDC->MoveTo (pEndPoint);
34: // Linie zum Endpunkt ziehen
35: pDC->LineTo (pStartPoint);
36:
37: // Ursprünglichen Stift wiederherstellen
38: pDC->SelectObject(&pOldPen);
39:
40: // Gerätekontext freigeben
41: m_pViewWnd->ReleaseDC(pDC);
42:
43: // Minute (Winkel) inkrementieren
44: if (++m_nMinute == 60)
45: {
46: // Nach vollem Umlauf Minute auf 0 zurücksetzen
47: m_nMinute = 0;
48: // Farbe inkrementieren
49: if (++m_crColor == 8)
50: // Wenn alle Farben durch, von vorn beginnen
51: m_crColor = 0;
52: }
53: }
Eine ganze Menge Code! Was bewirkt er? Um zu verstehen, was diese Funktion realisiert und wie der sich drehende Farbfächer in das Fenster gelangt, sehen wir uns den Code näher an.
Damit die verschiedenen Threads den Fächer effektiv nutzen können, zeichnet die Funktion bei jedem Aufruf nur jeweils eine Linie. Für einen vollständigen Kreis ist diese Funktion 60mal aufzurufen, einmal für jede »Minute« bei Drehung im Uhrzeigersinn. Nach jeder kompletten Umdrehung schaltet der Fächer zur nächsten Farbe in der Farbtabelle.
Bevor man überhaupt in das Fenster zeichnen kann, muß man zunächst den Gerätekontext
des Fensters holen. Dazu ruft man die Funktion GetDC
auf dem Zeiger des Ansichtsobjekts
auf:
CDC *pDC = m_pViewWnd->GetDC();
Die Funktion liefert einen Zeiger auf ein CDC
-Objekt zurück, d.h. auf eine MFC-Klasse,
die den Gerätekontext verkapselt.
Nachdem Sie über einen Zeiger auf den Gerätekontext verfügen, können Sie die
Member-Funktion SetMapMode
aufrufen, um den Abbildungsmodus festzulegen:
pDC->SetMapMode (MM_LOENGLISH);
Der Abbildungsmodus bestimmt, wie die x- und y-Koordinaten in Positionen auf dem
Bildschirm übersetzt werden. Der Modus MM_LOENGLISH
bildet eine logische Einheit auf
0,01 Zoll auf dem Bildschirm ab. Es sind verschiedene Abbildungsmodi verfügbar, die
jeweils logische Einheiten in bestimmte Maßeinheiten auf dem Bildschirm konvertieren.
Jetzt wenden Sie sich den Vorbereitungen zu, um die aktuelle Linie für den Farbfächer zu zeichnen. Zuerst berechnen Sie den Anfangspunkt für die zu zeichnende Linie. Dieser Punkt ist für alle vom Fächerobjekt gezeichneten Linien gleich. Anschließend berechnen Sie die Position des Viewports. Der Viewport dient als Ausgangspunkt für das Koordinatensystem, in dem die Zeichnung liegt.
Der Viewport ist der rechteckige Bereich eines Formulars, das im Container des Formulars angezeigt wird - mit anderen Worten ein Grafikfenster. |
Nachdem der Ursprung des Viewports berechnet ist, setzen Sie den Viewport mit der
Funktion SetViewportOrg
:
pDC->SetViewportOrg(org.x, org.y);
Der Zeichenbereich und der Startpunkt für die zu zeichnende Linie sind nun definiert. Als nächstes ist der Endpunkt der Linie zu bestimmen. Diese Berechnung steht in den folgenden drei Codezeilen:
double nRadians = (double) (m_nMinute * 6) * 0.017453292;
pEndPoint.x = (int) (m_iRadius * sin(nRadians));
pEndPoint.y = (int) (m_iRadius * cos(nRadians));
Die erste Zeile konvertiert die Minuten (nicht zu verwechseln mit Bogenminuten) in Grad. Das Ergebnis läßt sich dann in die Sinus- und Kosinusfunktionen einsetzen, um die x- und y-Koordinaten für das Zeichnen eines Kreises festzulegen. Daraus ergibt sich der Endpunkt der zu zeichnenden Linie.
Nunmehr verfügen Sie über den Anfangs- und Endpunkt der Linie und können einen Stift erzeugen, der die Linie zeichnet:
CPen pen(PS_SOLID, 0, m_crColors[m_crColor]);
Diese Anweisung legt fest, daß der Stift durchgehend und dünn sein soll und die aktuelle Farbe aus der Farbtabelle auszuwählen ist. Nachdem der zu verwendende Stift erzeugt wurde, selektieren Sie den Stift zum Zeichnen. Dabei übernehmen Sie den aktuellen Stift als Rückgabewert vom Gerätekontextobjekt:
CPen* pOldPen = pDC->SelectObject(&pen);
Jetzt läßt sich die Linie zeichnen. Dazu kommen die Funktionen MoveTo
und LineTo
zum Einsatz, die Ihnen mittlerweile vertraut sein dürften. Nach dem Zeichnen der
Linie geben Sie den Gerätekontext wieder frei, damit kein Ressourcenmangel in der
Anwendung auftritt:
m_pViewWnd->ReleaseDC(pDC);
Damit ist die Linie fertiggestellt. Jetzt inkrementieren Sie noch den Minutenzähler. Falls der Kreis geschlossen ist, setzen Sie den Zähler auf 0 zurück. Nach jedem vollständigen Kreis inkrementieren Sie den Farbenzähler und setzen ihn nach der achten Farbe wieder auf 0.
Damit Sie in dieser Funktion auf die trigonometrischen Funktionen zurückgreifen können,
binden Sie die Header-Datei math.h
in die Quelldatei der Fächerklasse (Spinner.cpp
) ein. Gehen Sie dazu an den Beginn der Quelldatei, und nehmen Sie die
#include
-Zeile mit der Header-Datei math.h
gemäß Listing 18.5 auf.
Listing 18.5: Die Quelldatei von CSpinner
1: // Spinner.cpp: Implementierung der Klasse CSpinner.
2: //
3: //////////////////////////////////////////////////////////////////////
4:
5: #include "stdafx.h"
6: #include <math.h>
7: #include "Tasking.h"
8: #include "Spinner.h"
Sie haben nun die Fächerklasse erzeugt, um den sich drehenden Farbfächer im Fenster zu zeichnen. Für die Fächer sind noch verschiedene Unterstützungsroutinen erforderlich. Man kann zwar der Dokumentklasse ein Feld hinzufügen, das die vier Fächer aufnimmt, trotzdem muß man noch berechnen, wo jeder Fächer im Anwendungsfenster erscheinen soll. Außerdem sind die Variablen für jeden Fächer festzulegen.
Diesen Code bringen Sie in der Dokumentklasse unter, wobei Sie mit dem Feld der
Fächer beginnen. Fügen Sie der Dokumentklasse (im Beispiel CTaskingDoc
) eine
Member-Variable hinzu, für die Sie den Typ mit CSpinner
, den Namen als m_cSpin[4]
und den Zugriff als Privat festlegen. Dann öffnen Sie den Quellcode der Dokumentklasse
und binden die Header-Datei für den Fächer ein, wie es Listing 18.6 zeigt.
Listing 18.6: Die Quelldatei von CTaskingDoc
1: // TaskingDoc.cpp : Implementierung der Klasse CTaskingDoc
2: //
3:
4: #include "stdafx.h"
5: #include "Tasking.h"
6:
7: #include "Spinner.h"
8: #include "TaskingDoc.h"
9: #include "TaskingView.h"
10: .
11: .
12: .
Zu den vorbereitenden Maßnahmen in der Initialisierungsphase der Anwendung gehört es, die Positionen aller vier Fächer zu bestimmen. Das Fenster ist durch die Kontrollkästchen, die die Fächer-Threads ein- und ausschalten, in vier etwa gleich große Quadranten unterteilt, so daß es naheliegt, den Fensterbereich in vier Rechtecke aufzuteilen und in jedem Rechteck einen Fächer unterzubringen.
Die Lage der einzelnen Fächer läßt sich am einfachsten berechnen, indem man eine Funktion erzeugt, die die Position für einen Fächer berechnet und den Fächer im Quadranten der jeweiligen Fächernummer plaziert. Übergibt man der Funktion einen Zeiger auf das Fächerobjekt, kann sie das Fächerobjekt direkt an der Position aktualisieren.
Fügen Sie zu diesem Zweck eine neue Elementfunktion in die Dokumentklasse (im
Beispiel die Klasse CTaskingDoc
) ein. Legen Sie den Funktionstyp als void
, die Deklaration
mit CalcPoint(int nID, CSpinner *pSpin)
und den Zugriff mit Privat fest.
Nehmen Sie in die Funktion den Code aus Listing 18.7 auf.
Listing 18.7: Die Funktion CalcPoint der Klasse CTaskingDoc
1: void CTaskingDoc::CalcPoint(int nID, CSpinner *pSpin)
2: {
3: RECT lWndRect;
4: CPoint pPos;
5: int iLength;
6: CTaskingView *pWnd;
7:
8: // Zeiger auf das Anzeigefenster holen
9: pWnd = (CTaskingView*)pSpin->GetViewWnd();
10: // Rechteck des Anzeigebereichs ermitteln
11: pWnd->GetClientRect(&lWndRect);
12: // Größe des Fächers berechnen
13: iLength = lWndRect.right / 6;
14: // Welcher Fächer wird plaziert?
15: switch (nID)
16: {
17: case 0: // Position des ersten Fächers
18: pPos.x = (lWndRect.right / 4) - iLength;
19: pPos.y = (lWndRect.bottom / 4) - iLength;
20: break;
21: case 1: // Position des zweiten Fächers
22: pPos.x = ((lWndRect.right / 4) * 3) - iLength;
23: pPos.y = (lWndRect.bottom / 4) - iLength;
24: break;
25: case 2: // Position des dritten Fächers
26: pPos.x = (lWndRect.right / 4) - iLength;
27: pPos.y = ((lWndRect.bottom / 4) * 3) - (iLength * 1.25);
28: break;
29: case 3: // Position des vierten Fächers
30: pPos.x = ((lWndRect.right / 4) * 3) - iLength;
31: pPos.y = ((lWndRect.bottom / 4) * 3) - (iLength * 1.25);
32: break;
33: }
34: // Größe des Fächers setzen
35: pSpin->SetLength(iLength);
36: // Lage des Fächers setzen
37: pSpin->SetPoint(pPos);
38: }
Diese Funktion ermittelt zuerst den Zeiger auf das Ansichtsfenster vom Fächerobjekt
durch den Aufruf der Funktion GetViewWnd
:
pWnd = (CTaskingView*)pSpin->GetViewWnd();
Indem man den Zeiger direkt vom Fächerobjekt ermittelt, spart man ein paar Schritte ein, um zu diesen Informationen zu gelangen.
Nachdem der Zeiger auf das Ansichtsobjekt vorhanden ist, kann man die Funktion
GetClientRect
des Fensters aufrufen, um die Größe des verfügbaren Zeichenbereichs
zu ermitteln:
pWnd->GetClientRect(&lWndRect);
Aus der Größe des verfügbaren Zeichenbereichs läßt sich eine sinnvolle Größe für den Farbfächer berechnen, indem man die Länge des Zeichenbereichs durch 6 teilt:
iLength = lWndRect.right / 6;
Teilt man den Zeichenbereich durch 4, erhält man eine Position in der Mitte des oberen linken Quadranten. Von diesem Punkt zieht man die Größe des Kreises ab, und man erhält die obere linke Ecke des Zeichenbereichs für den ersten Fächer:
pPos.x = (lWndRect.right / 4) - iLength;
pPos.y = (lWndRect.bottom / 4) - iLength;
Von diesem Punkt ausgehend erhält man die anderen Positionen, indem man den Mittelpunkt des Quadranten mit 3 multipliziert, um ihn in die Mitte des rechts daneben oder darunterliegenden Quadranten zu verschieben. Die Positionen der anderen drei Fächer lassen sich analog berechnen.
Nachdem die Länge und die Lage für den Fächer feststehen, übergibt man mit den
Funktionen SetLength
und SetPoint
die ermittelten Werte an den Fächer, für den sie
berechnet wurden:
pSpin->SetLength(iLength);
pSpin->SetPoint(pPos);
Da Sie die obige Funktion, mit der Sie die Lage jedes Fächers im Fenster berechnen, geschrieben haben, um bei einem Aufruf der Funktion nur mit jeweils einem Fächer zu arbeiten, braucht man eine Routine, die jeden Fächer initialisiert und dabei die obige Funktion einmal für jeden Fächer aufruft. Diese Funktion ist erforderlich, um einen Zeiger auf das Ansichtsobjekt zu ermitteln und den Zeiger dann an den Fächer weiterzureichen. Außerdem sind Zeiger auf die Variablen der Kontrollkästchen für die Fächer zu ermitteln, die von den unabhängig laufenden Threads verwendet werden. Der Code durchläuft dazu eine Schleife durch das Fächerfeld, legt dabei die Zeiger für jeden Fächer fest und übergibt dann den Fächer an die obige Funktion.
Fügen Sie zu diesem Zweck eine neue Member-Funktion in die Dokumentklasse
(CTaskingDoc
) ein. Legen Sie den Typ als void
und die Deklaration mit InitSpinners
fest. Als Zugriffsstatus wählen Sie die Option Privat, da die Funktion nur einmal aufzurufen
ist, wenn die Anwendung startet. Nehmen Sie in die neue Funktion den Code
aus Listing 18.8 auf.
Listing 18.8: Die Funktion InitSpinners der Klasse CTaskingDoc
1: void CTaskingDoc::InitSpinners()
2: {
3: int i;
4:
5: // Position der Ansicht holen
6: POSITION pos = GetFirstViewPosition();
7: // Ist Position gültig?
8: if (pos != NULL)
9: {
10: // Zeiger auf die Ansicht holen
11: CView* pView = GetNextView(pos);
12:
13: // Schleife durch die Fächer
14: for (i = 0; i < 4; i++)
15: {
16: // Zeiger auf die Ansicht setzen
17: m_cSpin[i].SetViewWnd(pView);
18: // Zeiger auf Fortsetzungskennzeichner initialisieren
19: m_cSpin[i].SetContinue(NULL);
20: switch (i)
21: {
22: case 1: // Zeiger auf Fortsetzungskennzeichner
23: // des ersten Threads setzen
24: m_cSpin[i].SetContinue(&((CTaskingView*)pView)->m_bThread1);
25: break;
26: case 3: // Zeiger auf zweiten Fortsetzungs-
27: // kennzeichner setzen
28: m_cSpin[i].SetContinue(&((CTaskingView*)pView)->m_bThread2);
29: break;
30: }
31: // Lage des Fächers berechnen
32: CalcPoint(i, &m_cSpin[i]);
33: }
34: }
35: }
Die Funktion durchläuft zuerst die Schritte, um einen Zeiger auf die Ansichtsklasse
vom Dokument zu ermitteln, wie Sie es zu Beginn von Tag 10 kennengelernt haben.
Nachdem Sie einen gültigen Zeiger auf die Ansicht besitzen, starten Sie eine Schleife,
um alle Fächer im Feld zu initialisieren. Sie rufen die Fächerfunktion SetViewWnd
auf,
um den Zeiger des Fächers auf das Ansichtsfenster zu setzen, und initialisieren dann
den Zeiger auf die Variable für das Kontrollkästchen des Fächers mit NULL
für alle Fächer.
Gehört der Fächer zu den beiden, die unabhängige Threads verwenden, übergeben
Sie einen Zeiger auf die entsprechende Variable des Kontrollkästchens. Nachdem
das alles eingerichtet ist, rufen Sie die Funktion CalcPoint
auf, die Sie gerade eben erstellt
haben, um die Position des Fächers im Ansichtsfenster zu berechnen.
Sie haben nun die Routinen erstellt, um alle Fächer zu initialisieren. Jetzt müssen Sie
gewährleisten, daß diese Routine beim Start der Anwendung aufgerufen wird. Es liegt
auf der Hand, daß man diese Logik in der Funktion OnNewDocument
in der Dokumentklasse
unterbringt. Diese Funktion wird aufgerufen, wenn die Anwendung startet. Somit
ist es logisch, die Initialisierung der Fächerobjekte von dieser Stelle auszulösen.
Der Code, den Sie in die Funktion OnNewDocument
in der Dokumentklasse einfügen,
ist in Listing 18.9 wiedergegeben.
Listing 18.9: Die Funktion OnNewDocument der Klasse CTaskingDoc
1: BOOL CTaskingDoc::OnNewDocument()
2: {
3: if (!CDocument::OnNewDocument())
4: return FALSE;
5:
6: // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen
7: // (SDI-Dokumente verwenden dieses Dokument)
8:
9: ///////////////////////
10: // EIGENER CODE, ANFANG
11: ///////////////////////
12:
13: // Die Fächer initialisieren
14: InitSpinners();
15:
16: ///////////////////////
17: // EIGENER CODE, ENDE
18: ///////////////////////
19:
20: return TRUE;
21: }
Eines der letzten Dinge, die Sie fürs erste in die Dokumentklasse aufnehmen, ist ein
Instrument, das den Aufruf der Funktion Draw
für einen bestimmten Fächer von außerhalb
der Dokumentklasse erlaubt. Da das Fächerfeld als private Variable deklariert ist,
können Objekte von außerhalb nicht auf die Fächer zugreifen. Daher sind Mechanismen
für derartige Zugriffe vorzusehen. Mit einer neuen Member-Funktion in der Dokumentklasse
kann man diesen Zugriff bereitstellen. Legen Sie für die Funktion den
Typ void
fest, spezifizieren Sie die Funktionsdeklaration mit einem Namen und einem
einzelnen Integer-Argument für die Fächernummer wie etwa DoSpin(int nIndex)
,
und wählen Sie als Zugriffsstatus die Option Public. In den Funktionsrumpf schreiben
Sie den Code aus Listing 18.10, um den eigentlichen Aufruf des angegebenen Fächers
zu realisieren.
Listing 18.10: Die Funktion DoSpin der Klasse CTaskingDoc
1: void CTaskingDoc::DoSpin(int nIndex)
2: {
3: // Den Fächer drehen
4: m_cSpin[nIndex].Draw();
5: }
Nachdem Sie nun die unterstützende Funktionalität unter Dach und Fach haben, ist es
an der Zeit, Ihre Aufmerksamkeit den verschiedenen Threads zu widmen, die für die
Drehung der einzelnen Fächer verantwortlich sind. Als erstes fügen Sie die Threads
hinzu, die im Leerlauf der Anwendung ausgeführt werden. Für die zwei Kontrollkästchen
sehen Sie eine Behandlungsroutine für das Klickereignis vor, so daß die Variablen
für diese beiden Kontrollkästchen mit dem Fenster synchron bleiben. Weiterhin
schreiben Sie Code in die OnIdle
-Funktion der Anwendung, um diese beiden Fächer
zu starten, wenn sich die Anwendung im Leerlauf befindet und die Kontrollkästchen
für diese beiden Fächer-Threads eingeschaltet sind.
Die Funktion OnIdle
prüft die Werte der beiden Variablen für die Kontrollkästchen,
die festlegen, ob die jeweilige Task laufen soll. Die Anwendung muß also nur dafür
Sorge tragen, daß die Variablen im Ansichtsobjekt mit den Steuerelementen im Fenster
synchronisiert werden, wenn man auf die Kontrollkästchen klickt. Dazu ist lediglich
die Funktion UpdateData
aufzurufen. Um die OnIdle
-Tasks starten und anhalten
zu können, muß man nur eine einzige Behandlungsroutine für beide On Idle Thread-
Kontrollkästchen hinzufügen und dann die Funktion UpdateData
in dieser Behandlungsroutine
aufrufen.
Öffnen Sie dazu den Klassen-Assistenten, und wählen Sie die Ansichtsklasse (in diesem
Fall CTaskingView
) aus. Markieren Sie eines der On Idle-Kontrollkästchen, und
fügen Sie eine Funktion für das Ereignis BN_CLICKED
hinzu. Ändern Sie den vorgeschlagenen
Namen der Funktion in OnCbonidle
, und klicken Sie auf OK. Wiederholen
Sie das gleiche für das andere On Idle-Kontrollkästchen. Damit haben Sie festgelegt,
daß beide Ereignisse denselben Code verwenden. Klicken Sie auf Code bearbeiten,
und fügen Sie den Code aus Listing 18.11 in die Funktion ein.
Listing 18.11: Die Funktion OnCbonidle der Klasse CTaskingView
1: void CTaskingView::OnCbonidle()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement-
ÂBenachrichtigung hier einfügen
4:
5: ///////////////////////
6: // EIGENER CODE, ANFANG
7: ///////////////////////
8:
9: // Variablen mit Dialogfeld synchronisieren
10: UpdateData(TRUE);
11:
12: ///////////////////////
13: // EIGENER CODE, ENDE
14: ///////////////////////
15: }
Wenn Sie den Quellcode der Anwendungsklasse (CTaskingApp
) untersuchen, fällt Ihnen
sicherlich auf, daß es hier keine OnIdle
-Funktion gibt. Die ganze Funktionalität,
die die OnIdle
-Funktion per Vorgabe ausführen muß, befindet sich in der Basisklasse
der Anwendungsklasse, die für Ihr Projekt erzeugt wurde. Der einzige Grund, eine
OnIdle
-Funktion in Ihre Anwendungsklasse aufzunehmen, besteht darin, daß Ihre Anwendung
eine bestimmte Funktionalität während dieses Ereignisses realisieren muß.
Im Ergebnis müssen Sie diese Behandlungsroutine explizit in Ihre Anwendung mit Hilfe
des Klassen-Assistenten einbinden.
Nachdem Sie die OnIdle
-Funktion in Ihre Anwendungsklasse aufgenommen haben,
was muß sie tun? Zuerst ist ein Zeiger auf die Ansicht zu ermitteln, damit sich der Zustand
der Variablen für die Kontrollkästchen prüfen läßt. Als nächstes muß die Funktion
einen Zeiger auf die Dokumentklasse holen, damit der Aufruf der Funktion DoSpin
möglich ist, um das entsprechende Fächerobjekt auszulösen. Der Schlüssel für
beide Aktionen liegt darin, Zeiger auf jedes dieser Objekte zu ermitteln. Wenn Sie sich
damit beschäftigen, auf welche Weise diese Zeiger zu erhalten sind, stellen Sie fest,
daß die angegebene Reihenfolge umzukehren ist. Sie müssen einen Zeiger auf das Dokumentobjekt
holen, damit Sie einen Zeiger auf die Ansicht erhalten. Um jedoch
einen Zeiger auf das Dokument zu erhalten, muß man durch die Dokumentvorlage gehen
und einen Zeiger auf die Vorlage holen, bevor man einen Zeiger auf das Dokument
erhalten kann. Jeder dieser Schritte erfordert die gleiche Folge von Ereignissen
- zuerst die Position des ersten Objekts holen und dann einen Zeiger auf das Objekt
auf dieser Position ermitteln. Es ist also folgendes zu erledigen: Die Position der ersten
Dokumentvorlage ermitteln und dann einen Zeiger auf die Dokumentvorlage an dieser
Position holen. Als nächstes nehmen Sie die Dokumentvorlage, um die Position des
ersten Dokuments zu ermitteln, und dann verwenden Sie die Dokumentvorlage, um
einen Zeiger auf das Dokument an dieser ersten Position zu holen. Schließlich verwenden
Sie das Dokument, um die Position der ersten Ansicht zu ermitteln, und verwenden
dann erneut das Dokument, um einen Zeiger auf die Ansicht an der angegebenen
Position zu holen. Nachdem Sie einen Zeiger auf die Ansicht besitzen, können Sie den
Wert der Kontrollkästchen testen und den betreffenden Fächer aufrufen.
Um diese Funktionalität in Ihre Anwendung einzubauen, nehmen Sie mit dem Klassen-Assistenten
eine Funktion für die Nachricht OnIdle
in die Anwendungsklasse (in
diesem Fall CTaskingApp
) auf. Anschließend klicken Sie auf die Schaltfläche Code bearbeiten
und fügen den Code von Listing 18.12 ein.
Listing 18.12: Die Funktion OnIdle der Klasse CTaskingApp
1: BOOL CTaskingApp::OnIdle(LONG lCount)
2: {
3: // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
4:
5: // Position der ersten Dokumentvorlage holen
6: POSITION pos = GetFirstDocTemplatePosition();
7: // Ist Position der Dokumentvorlage gültig?
8: if (pos)
9: {
10: // Zeiger auf die Dokumentvorlage ermitteln
11: CDocTemplate* pDocTemp = GetNextDocTemplate(pos);
12: // Ist Zeiger gültig?
13: if (pDocTemp)
14: {
15: // Position des ersten Dokuments holen
16: POSITION dPos = pDocTemp->GetFirstDocPosition();
17: // Ist Position des Dokuments gültig?
18: if (dPos)
19: {
20: // Zeiger auf das Dokument ermitteln
21: CTaskingDoc* pDocWnd =
22: (CTaskingDoc*)pDocTemp->GetNextDoc(dPos);
23: // Ist Zeiger gültig?
24: if (pDocWnd)
25: {
26: // Position der Ansicht ermitteln
27: POSITION vPos = pDocWnd->GetFirstViewPosition();
28: // Ist Ansichtsposition gültig?
29: if (vPos)
30: {
31: // Zeiger auf die Ansicht holen
32: CTaskingView* pView = (CTaskingView*)pDocWnd->
ÂGetNextView(vPos);
33: // Ist Zeiger gültig?
34: if (pView)
35: {
36: // Fächer für ersten Leerlauf-Thread drehen?
37: if (pView->m_bOnIdle1)
38: // Ersten Leerlauf-Thread drehen
39: pDocWnd->DoSpin(0);
40: // Zweiten Leerlauf-Thread drehen?
41: if (pView->m_bOnIdle2)
42: // Zweiten Leerlauf-Thread drehen
43: pDocWnd->DoSpin(2);
44: }
45: }
46: }
47: }
48: }
49: }
50:
51: // Leerlaufverarbeitung der Basisklasse aufrufen
52: return CWinApp::OnIdle(lCount);
53: }
Wenn Sie die Anwendung jetzt kompilieren und ausführen, sollten Sie die Kontrollkästchen
On Idle Thread
einschalten und Farbfächer wie in Abbildung 18.7 zeichnen
können, solange Sie die Maus bewegen. In dem Moment aber, in dem Sie die Anwendung
im Leerlauf - ohne Mausbewegung oder irgend etwas anderes - arbeiten lassen,
dreht sich der Fächer nicht mehr.
Abbildung 18.7:
Ein On Idle-Thread, der einen Farbfächer zeichnet
Es ist nicht gerade sehr praktisch, die Maus in Bewegung zu halten, damit die Anwendung
die Aufgaben, die eigentlich im Leerlauf stattfinden sollen, fortlaufend ausführt.
Es muß eine Möglichkeit geben, die Funktion OnIdle
aufzurufen, solange sich die Anwendung
im Leerlauf befindet. Natürlich gibt es die. In der letzten Zeile der OnIdle
-
Funktion ist zu sehen, daß die Funktion OnIdle
den Ergebniswert der OnIdle
-Funktion
aus der Basisklasse zurückgibt. Und diese Funktion liefert den Wert FALSE
, sobald keine
Leerlaufaktivitäten mehr auszuführen sind.
Man muß also erreichen, daß die Funktion OnIdle
immer den Wert TRUE
zurückliefert.
Damit wird die Funktion OnIdle
fortlaufend aufgerufen, wann immer die Anwendung
im Leerlauf arbeitet. Wenn Sie den Aufruf der OnIdle
-Funktion der Basisklasse - wie
in Listing 18.13 angegeben - in den ersten Teil der Funktion verschieben und dann
TRUE
zurückgeben, dreht sich der Fächer ununterbrochen, unabhängig davon, wie lange
die Anwendung im Leerlauf arbeitet.
Listing 18.13: Die modifizierte Funktion OnIdle der Klasse CTaskingApp
1: BOOL CTaskingApp::OnIdle(LONG lCount)
2: {
3: // TODO: Speziellen Code hier einfügen und/oder Basisklasse aufrufen
4:
5: // Leerlaufverarbeitung der Basisklasse aufrufen
6: CWinApp::OnIdle(lCount);
7:
8: // Position der ersten Dokumentvorlage holen
9: POSITION pos = GetFirstDocTemplatePosition();
10: // Ist Position der Dokumentvorlage gültig?
11: if (pos)
12: {
.
.
.
51: }
52: return TRUE;
53: }
Wenn Sie die Anwendung jetzt kompilieren und ausführen, können Sie die OnIdle
-
Tasks einschalten und eine fortlaufende Drehung beobachten, selbst wenn Sie die
Maus nicht bewegen. Falls Sie jedoch eines der Menüs aktivieren oder das Info-Dialogfeld
öffnen, werden beide Tasks vollständig gestoppt, wie es Abbildung 18.8 zeigt.
Der Grund dafür liegt darin, daß geöffnete Menüs und alle geöffneten modalen Dialogfelder
den Aufruf der OnIdle
-Funktion unterbinden. Eine der Beschränkungen der
OnIdle
-Verarbeitung ist es, daß bestimmte Funktionen der Anwendung verhindern,
daß die Tasks weiterhin laufen.
Abbildung 18.8:
Ein On Idle-Thread, der durch das Menü angehalten wird
Nachdem Sie sich damit bekannt gemacht haben, was alles zu einer OnIdle
-Task gehört,
geht es nun darum, unabhängige Threads in einer Anwendung zu realisieren. Als
erstes fügen Sie eine Hauptfunktion für die Threads hinzu. Weiterhin ist der Code zum
Starten und Anhalten der Threads aufzunehmen. Schließlich kommt noch der Code
für die Kontrollkästchen der unabhängigen Threads hinzu, um diese Threads starten
und stoppen zu können.
Bevor Sie irgendeinen unabhängigen Thread starten können, muß der Thread wissen, was zu tun ist. Dazu erzeugen Sie eine Thread-Hauptfunktion, die der Thread ausführt, wenn er startet. Der Thread endet, sobald die Funktion endet. Demzufolge muß diese Funktion als Hauptsteuerung des Threads agieren und den Thread so lange laufen lassen, solange es irgend etwas für den Thread zu tun gibt. Ist die Arbeit des Threads erledigt, muß die Funktion zurückkehren.
Wenn man eine Funktion als Hauptfunktion für einen Thread erzeugt, kann man ihr einen einzelnen Parameter übergeben. Dieser Parameter ist ein Zeiger auf Informationen, die der Thread für die Ausführung seiner Aufgaben benötigt. Bei der Anwendung, die Sie in diesem Kapitel erstellen, kann der Parameter ein Zeiger auf den Fächer sein, auf dem der Thread operiert. Alles andere, was der Thread benötigt, läßt sich aus dem Fächerobjekt herausziehen.
Nachdem der Thread einen Zeiger auf seinen Fächer hat, kann er einen Zeiger auf die
Variable für das Kontrollkästchen ermitteln, das darüber Auskunft gibt, ob die Drehung
weiterlaufen oder sich der Thread selbst anhalten soll. Solange die Variable TRUE
ist, soll der Thread die Drehung weiter ausführen.
Fügen Sie zu diesem Zweck der Dokumentklasse Ihrer Anwendung eine neue Member-Funktion
hinzu. Legen Sie den Typ der Funktion mit UINT
, die Funktionsdeklaration
mit ThreadFunc(LPVOID pParam)
und den Zugriff als Privat fest. Den Thread starten
Sie aus der Dokumentklasse heraus, so daß diese Funktion für keine andere Klasse
sichtbar sein muß. In die Funktion übernehmen Sie den Code aus Listing 18.14.
Listing 18.14: Die Funktion ThreadFunc der Klasse CTaskingDoc
1: UINT CTaskingDoc::ThreadFunc(LPVOID pParam)
2: {
3: // Argument in einen Zeiger auf den Fächer
4: // für diesen Thread konvertieren
5: CSpinner* lpSpin = (CSpinner*)pParam;
6: // Zeiger auf Fortsetzungsflag holen
7: BOOL* pbContinue = lpSpin->GetContinue();
8:
9: // Schleife, solange Fortsetzungsflag TRUE ist
10: while (*pbContinue)
11: // Fächer drehen
12: lpSpin->Draw();
13: return 0;
14: }
Mit der Funktion ThreadFunc
können Sie nun die unabhängigen Threads aufrufen.
Jetzt brauchen Sie noch eine Möglichkeit, die Threads zu steuern, d.h. die Threads zu
starten und anzuhalten. Dafür müssen Sie eine Reihe von Zeigern für CWinThread
-Objekte,
die die Threads verkapseln, speichern können. Die Zeiger fügen Sie als Variablen
in das Dokumentobjekt ein und verwenden sie, um den Rückgabewert der Funktion
AfxBeginThread
, mit der Sie beide Threads starten, aufzunehmen.
Fügen Sie als erstes eine neue Member-Variable in die Dokumentklasse ein. Legen Sie
den Variablentyp mit CWinThread*
, den Variablennamen als m_pSpinThread[2]
und
den Zugriff auf die Variable mit Privat fest. Auf diese Weise erhalten Sie ein zweielementiges
Feld für die Aufnahme der oben genannten Variablen.
Sie verfügen jetzt über einen Platz, an dem Sie die Zeiger auf die beiden Threads speichern können. Als nächstes schreiben Sie den Code, um die Threads zu starten. Für beide Threads können Sie ein und dieselbe Funktion vorsehen. Läuft der Thread noch nicht, starten Sie ihn, ansonsten warten Sie, bis sich der Thread selbst anhält. Die Funktion muß wissen, auf welchen Thread sich der Aufruf bezieht und ob der Thread zu starten oder anzuhalten ist.
Nehmen Sie dazu eine neue Member-Funktion in die Dokumentklasse auf. Legen Sie
den Funktionstyp mit void
, die Funktionsdeklaration als SuspendSpinner(int nIndex,
BOOL bSuspend)
und den Zugriff auf die Funktion mit Public fest. Schalten Sie
außerdem das Kontrollkästchen Statisch ein. In die Funktion schreiben Sie den Code
gemäß Listing 18.15.
Listing 18.15: Die Funktion SuspendSpinner der Klasse CTaskingDoc
1: void CTaskingDoc::SuspendSpinner(int nIndex, BOOL bSuspend)
2: {
3: // Thread anhalten?
4: if (!bSuspend)
5: {
6: // Ist Zeiger für den Thread gültig?
7: if (m_pSpinThread[nIndex])
8: {
9: // Handle für den Thread holen
10: HANDLE hThread = m_pSpinThread[nIndex]->m_hThread;
11: // Auf Absterben des Threads warten
12: ::WaitForSingleObject (hThread, INFINITE);
13: }
14: }
15: else // Thread laufen lassen
16: {
17: int iSpnr;
18: // Welchen Fächer verwenden?
19: switch (nIndex)
20: {
21: case 0:
22: iSpnr = 1;
23: break;
24: case 1:
25: iSpnr = 3;
26: break;
27: }
28: // Thread starten, Zeiger auf Fächer übergeben
29: m_pSpinThread[nIndex] = AfxBeginThread(ThreadFunc,
30: (LPVOID)&m_cSpin[iSpnr]);
31: }
32: }
Als erstes prüft die Funktion, ob der Thread zu starten oder anzuhalten ist. Wird der
Thread gestoppt, prüft die Funktion als nächstes, ob der Zeiger auf den Thread gültig
ist. Bei einem gültigen Zeiger rufen Sie den Handle für den Thread ab, indem Sie den
Wert der Handle-Eigenschaft der Klasse CWinThread
lesen:
HANDLE hThread = m_pSpinThread[nIndex]->m_hThread;
Mit diesem Handle rufen Sie die Funktion WaitForSingleObjekt
auf, um darauf zu
warten, daß sich der Thread selbst stoppt.
::WaitForSingleObject (hThread, INFINITE);
Die Windows-API-Funktion WaitForSingleObject
teilt dem Betriebssystem mit, daß
Sie darauf warten, daß der per Handle bezeichnete Thread anhält. Das zweite Argument
an diese Funktion gibt an, wie lange Sie warten wollen. Der Wert INFINITE
bedeutet,
daß Sie unbegrenzt lange warten wollen, bis dieser Thread stoppt. Wenn Sie
eine Zeitüberschreitung spezifizieren und der Thread nicht innerhalb dieser Zeit anhält,
liefert die Funktion einen Wert zurück, der angibt, ob der Thread gestoppt hat.
Da hier INFINITE
als Zeitüberschreitung festgelegt ist, brauchen Sie sich nicht um den
Rückgabewert der Funktion zu kümmern, da die Funktion erst dann zurückkehrt,
wenn der Thread stoppt.
Wenn der Thread noch nicht läuft, bestimmt die Funktion den zu verwendenden Fächer
und startet dann den Thread durch Aufruf der Funktion AfxBeginThread
.
m_pSpinThread[nIndex] = AfxBeginThread(ThreadFunc,
(LPVOID)&m_cSpin[iSpnr]);
Als erstes Argument übergeben Sie die für den Thread aufzurufende Hauptfunktion, als zweites Argument die Adresse des Fächers, den der Thread verwenden soll.
Sie verfügen nun über die Mechanismen, um unabhängige Threads zu starten und zu stoppen. Jetzt müssen Sie noch die Threads über die Kontrollkästchen im Fenster starten und anhalten können. Sind beide Kontrollkästchen eingeschaltet, starten Sie beide Threads. Wenn die Kontrollkästchen ausgeschaltet sind, müssen beide Threads gestoppt werden. Die zweite Aufgabe ist leicht: Solange die Variable, die an das Kontrollkästchen gebunden ist, mit dem Steuerelement synchronisiert ist, stoppt sich der Thread selbst, sobald Sie das Kontrollkästchen ausschalten. Wenn das Kontrollkästchen jedoch eingeschaltet ist, müssen Sie die eben erstellte Funktion des Dokumentobjekts (zum Starten der Threads) aufrufen.
Um die Funktionalität für das erste der beiden Thread-Kontrollkästchen zu realisieren,
fügen Sie mit dem Klassen-Assistenten eine Funktion für das Ereignis BN_CLICKED
des
Kontrollkästchens hinzu und übernehmen den Code aus Listing 18.16 in die Funktion.
Listing 18.16: Die Funktion OnCbthread1 der Klasse CTaskingView
1: void CTaskingView::OnCbthread1()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement-
ÂBenachrichtigung hier einfügen
4:
5: ///////////////////////
6: // EIGENER CODE, ANFANG
7: ///////////////////////
8:
9: // Variablen mit Dialogfeld synchronisieren
10: UpdateData(TRUE);
11:
12: // Zeiger auf Dokument holen
13: CTaskingDoc* pDocWnd = (CTaskingDoc*)GetDocument();
14: // Ist Zeiger gültig?
15: ASSERT_VALID(pDocWnd);
16:
17: // Fächer-Thread anhalten oder starten
18: pDocWnd->SuspendSpinner(0, m_bThread1);
19:
20: ///////////////////////
21: // EIGENER CODE, ENDE
22: ///////////////////////
23: }
Die Funktion ruft zuerst UpdateData
auf, um die Variablen mit den Steuerelementen
im Fenster zu synchronisieren. Als nächstes wird ein Zeiger auf das Dokument abgerufen.
Mit dem gültigen Zeiger rufen Sie die Funktion SuspendSpinner
des Dokuments
auf, wobei Sie den ersten Thread spezifizieren. Außerdem übergeben Sie den aktuellen
Wert der mit dem Kontrollkästchen verbundenen Variablen, damit die Funktion
weiß, ob der Thread zu starten oder zu stoppen ist.
Um die gleiche Funktionalität für das andere Thread-Kontrollkästchen zu realisieren, führen Sie die gleichen Schritte aus, fügen aber den Code aus Listing 18.17 ein.
Listing 18.17: Die Funktion OnCbthread2 der Klasse CTaskingView
1: void CTaskingView::OnCbthread2()
2: {
3: // TODO: Code für die Behandlungsroutine der Steuerelement-
ÂBenachrichtigung hier einfügen
4:
5: ///////////////////////
6: // EIGENER CODE, ANFANG
7: ///////////////////////
8:
9: // Variablen mit Dialogfeld synchronisieren
10: UpdateData(TRUE);
11:
12: // Zeiger auf Dokument holen
13: CTaskingDoc* pDocWnd = (CTaskingDoc*)GetDocument();
14: // Ist Zeiger gültig?
15: ASSERT_VALID(pDocWnd);
16:
17: // Fächer-Thread anhalten oder starten
18: pDocWnd->SuspendSpinner(1, m_bThread2);
19:
20: ///////////////////////
21: // EIGENER CODE, ENDE
22: ///////////////////////
23: }
Nachdem Sie nun die unabhängigen Threads starten und stoppen können, kompilieren und starten Sie die Anwendung. Überzeugen Sie sich davon, daß sich die unabhängigen Threads über die betreffenden Kontrollkästchen starten und stoppen lassen.
Wenn Sie in dieser Entwicklungsphase ein wenig mit der Anwendung experimentieren,
stellen Sie einen kleinen Unterschied zwischen den beiden Arten von Threads
fest. Wenn alle Threads laufen und Sie die Maus ausgiebig bewegen, ist zu bemerken,
daß die OnIdle
-Fächer langsamer drehen (außer wenn Sie mit einem sehr schnellen
Computer arbeiten). Die unabhängigen Threads beanspruchen einen guten Teil der
Prozessorzeit vom Haupt-Thread der Anwendung und lassen kaum Prozessorzeit für
den Leerlauf übrig. Im Ergebnis ist es einfacher, die Anwendung beschäftigt zu halten.
Wenn Sie Menüs aktivieren oder das Info-Fenster öffnen, ist weiterhin festzustellen,
daß zwar die OnIdle
-Tasks komplett stoppen, die unabhängigen Threads aber weiterlaufen,
wie es Abbildung 18.9 verdeutlicht. Die beiden Threads sind vollkommen unabhängige
Prozesse innerhalb Ihrer Anwendung, so daß sie nicht vom übrigen Teil der
Anwendung beeinflußt werden.
Abbildung 18.9:
Die Threads werden nicht durch das Menü beeinflußt.
Vielleicht sind Sie der Meinung, daß die Anwendung fertiggestellt ist. Versuchen Sie aber nun, die Anwendung zu schließen, während ein oder beide unabhängigen Threads laufen. Leider erhalten Sie einen unwillkommenen Hinweis wie in Abbildung 18.10, so daß Sie noch etwas unternehmen müssen. Es scheint, daß laufende Threads beim Schließen der Anwendung einen Absturz herbeiführen können.
Abbildung 18.10:
Fehlermeldung der Anwendung
Auch wenn Sie die Anwendung schließen, laufen die Threads unbekümmert weiter. Sobald die Threads den Wert der Variablen testen, die angibt, ob der Thread weiterlaufen oder den zugehörigen Fächer drehen soll, greifen sie auf ein Speicherobjekt zu, das nicht mehr existiert.
Dieses Problem verursacht einen der grundlegendsten und schwerwiegendsten Speicherfehler in einer Anwendung, und Sie sollten diesen Fehler eliminieren, bevor Sie irgend jemandem die Anwendung zum Einsatz übergeben.
Um den Fehler zu unterbinden, stoppen Sie beide Threads, bevor Sie erlauben, die
Anwendung zu schließen. Diese Aktion realisiert man am besten in der Behandlungsroutine
für die Nachricht WM_DESTROY
in der Ansichtsklasse. Mit dieser Nachricht wird
der Ansichtsklasse mitgeteilt, daß vor dem Schließen der Anwendung noch alle erforderlichen
Aufräumarbeiten zu erledigen sind. Mit dem Code in der Behandlungsfunktion
setzen Sie die Variablen der beiden Kontrollkästchen auf FALSE
, so daß sich die
Threads selbst anhalten. Dann rufen Sie die Funktion SuspendSpinner
für jeden
Thread auf, um sich davon zu überzeugen, daß beide Threads gestoppt haben, bevor
Sie der Anwendung das Schließen erlauben. Es ist kein Aufruf von UpdateData
erforderlich,
um die Variablen mit den Steuerelementen zu synchronisieren, da der Benutzer
nicht mehr sehen muß, wenn Sie den Wert der Kontrollkästchen ändern.
Nehmen Sie zu diesem Zweck eine Behandlungsroutine für die Nachricht WM_DESTROY
in die Ansichtsklasse Ihrer Anwendung auf. Der Anwendungs-Assistent erstellt diese
Funktion normalerweise nicht für die Ansichtsklasse, so daß Sie sie bei Bedarf in der
abgeleiteten Ansichtsklasse hinzufügen müssen. In die Funktion übernehmen Sie den
Code von Listing 18.18.
Listing 18.18: Die Funktion OnDestroy der Klasse CTaskingView
1: void CTaskingView::OnDestroy()
2: {
3: CFormView::OnDestroy();
4:
5: // TODO: Code für die Behandlungsroutine für Nachrichten hier einfügen
6:
7: ///////////////////////
8: // EIGENER CODE, ANFANG
9: ///////////////////////
10:
11: // Läuft der erste Thread?
12: if (m_bThread1)
13: {
14: // Stoppen für ersten Thread veranlassen
15: m_bThread1 = FALSE;
16: // Zeiger auf das Dokument holen
17: CTaskingDoc* pDocWnd = (CTaskingDoc*)GetDocument();
18: // Ist der Zeiger gültig?
19: ASSERT_VALID(pDocWnd);
20:
21: // Fächer-Thread anhalten
22: pDocWnd->SuspendSpinner(0, m_bThread1);
23: }
24: // Läuft der zweite Thread?
25: if (m_bThread2)
26: {
27: // Stoppen für zweiten Thread veranlassen
28: m_bThread2 = FALSE;
29: // Zeiger auf das Dokument holen
30: CTaskingDoc* pDocWnd = (CTaskingDoc*)GetDocument();
31: // Ist der Zeiger gültig?
32: ASSERT_VALID(pDocWnd);
33:
34: // Fächer-Thread anhalten
35: pDocWnd->SuspendSpinner(1, m_bThread2);
36: }
37:
38: ///////////////////////
39: // EIGENER CODE, ENDE
40: ///////////////////////
41: }
Die Funktion führt genau das aus, was zu tun ist. Zuerst prüft sie die Variablen der
Kontrollkästchen. Wenn eines der Kontrollkästchen den Wert TRUE
aufweist, wird die
entsprechende Variable auf FALSE
gesetzt, ein Zeiger auf das Dokument ermittelt und
die Funktion SuspendSpinner
für den betreffenden Thread aufgerufen. Wenn Sie nun
die Anwendung schließen, stürzt sie nicht mehr ab, auch wenn gerade unabhängige
Threads laufen.
Der heutige Tag hat eine Menge Stoff gebracht. Es ging um die verschiedenen Möglichkeiten, wie man mehrere Aufgaben (sogenannte Tasks) mit Anwendungen gleichzeitig ausführen kann. Sie haben wichtige Punkte kennengelernt, die zu beachten sind, wenn man die Anwendungen mit Multitasking-Fähigkeiten ausstatten möchte. Es wurde gezeigt, wie sich Tasks im Leerlauf einer Anwendung ausführen lassen. In diesem Zusammenhang wurden auch die Einschränkungen und Nachteile dieser Lösung behandelt. Weiterhin haben Sie gelernt, wie man unabhängige Threads in einer Anwendung erstellt. Die Threads arbeiten vollkommen unabhängig von der übrigen Anwendung. In der heutigen Beispielanwendung haben Sie die beiden Lösungsansätze verwendet, so daß Sie damit Erfahrung sammeln können.
Frage:
Wie kann ich die andere Version von AfxBeginThread
einsetzen, um einen
Thread in einer benutzerdefinierten Klasse zu verkapseln?
Antwort:
Erstens dient die andere Version von AfxBeginThread hauptsächlich dazu,
Threads für Benutzeroberflächen zu erzeugen. Die in der heutigen Beispielanwendung
eingesetzte Version ist für sogenannte Worker-Threads vorgesehen,
die sofort mit einer bestimmten Task beginnen. Wenn Sie einen Thread für
eine Benutzeroberfläche erzeugen möchten, müssen Sie Ihre benutzerdefinierte
Klasse von der Klasse CWinThread ableiten. Als nächstes sind mehrere
Vorgängerfunktionen in der benutzerdefinierten Klasse zu überschreiben.
Nachdem die Klasse für den Einsatz vorbereitet ist, holen Sie mit dem Makro
RUNTIME_CLASS einen Zeiger auf die Laufzeitklasse Ihrer Klasse und übergeben
diesen Zeiger folgendermaßen an die Funktion AfxBeginThread:
CWinThread* pMyThread =
AfxBeginThread(RUNTIME_CLASS(CMyThreadClass));
Frage:
Kann ich mit SuspendThread
und ResumeThread
meine unabhängigen Threads in
der Beispielanwendung starten und stoppen?
Antwort:
Ja. Es sind aber ein paar wesentliche Änderungen an der Anwendung erforderlich.
Erstens müssen Sie in der Funktion OnNewDocument die beiden Thread-
Zeiger mit NULL initialisieren, wie es Listing 18.19 zeigt.
Listing 18.19: Die modifizierte Funktion OnNewDocument der Klasse CTaskingDoc
1: BOOL CTaskingDoc::OnNewDocument()
2: {
3: if (!CDocument::OnNewDocument())
4: return FALSE;
5:
6: // ZU ERLEDIGEN: Hier Code zur Reinitialisierung einfügen
7: // (SDI-Dokumente verwenden dieses Dokument)
8:
9: ///////////////////////
10: // EIGENER CODE, ANFANG
11: ///////////////////////
12:
13: // Die Fächer initialisieren
14: InitSpinners();
15:
16: // Die Thread-Zeiger initialisieren
17: m_pSpinThread[0] = NULL;
18: m_pSpinThread[1] = NULL;
19:
20: ///////////////////////
21: // EIGENER CODE, ENDE
22: ///////////////////////
23:
24: return TRUE;
25: }
Als nächstes modifizieren Sie die Thread-Funktion gemäß Listing 18.20, damit
sich der Thread nicht selbst anhält, sondern weiter in der Schleife läuft,
wenn die Variable des Kontrollkästchens den Wert FALSE
aufweist.
Listing 18.20: Die modifizierte Funktion ThreadFunc der Klasse CTaskingDoc
1: UINT CTaskingDoc::ThreadFunc(LPVOID pParam)
2: {
3: // Argument in einen Zeiger auf den Fächer
4: // für diesen Thread konvertieren
5: CSpinner* lpSpin = (CSpinner*)pParam;
6: // Zeiger auf Fortsetzungsflag holen
7: BOOL* pbContinue = lpSpin->GetContinue();
8:
9: // Schleife, solange Fortsetzungsflag TRUE ist
10: while (TRUE)
11: // Fächer drehen
12: lpSpin->Draw();
13: return 0;
14: }
Schließlich ist die Funktion SuspendSpinner
entsprechend Listing 18.21 zu
modifizieren, so daß sie bei einem gültigen Thread-Zeiger die Funktion SuspendThread
auf dem Thread-Zeiger aufruft, um den Thread zu stoppen, und
die Funktion ResumeThread
, um den Thread erneut zu starten.
Listing 18.21: Die modifizierte Funktion SuspendSpinner der Klasse CTaskingDoc
1: void CTaskingDoc::SuspendSpinner(int nIndex, BOOL bSuspend)
2: {
3: // Thread anhalten?
4: if (!bSuspend)
5: {
6: // Ist Zeiger für den Thread gültig?
7: if (m_pSpinThread[nIndex])
8: {
9: // Handle für den Thread holen
10: m_pSpinThread[nIndex]->SuspendThread();
11: }
12: }
13: else // Thread laufen lassen
14: {
15: // Ist Zeiger für den Thread gültig?
16: if (m_pSpinThread[nIndex])
17: {
18: // Thread fortsetzen
19: m_pSpinThread[nIndex]->ResumeThread();
20: }
21: else
22: {
23: int iSpnr;
24: // Welchen Fächer verwenden?
25: switch (nIndex)
26: {
27: case 0:
28: iSpnr = 1;
29: break;
30: case 1:
31: iSpnr = 3;
32: break;
33: }
34: // Thread starten, Zeiger auf Fächer übergeben
35: m_pSpinThread[nIndex] = AfxBeginThread(ThreadFunc,
36: (LPVOID)&m_cSpin[iSpnr]);
37: }
38: }
39: }
1. Wann wird die Funktion OnIdle
aufgerufen?
2. Wie kann man den wiederholten Aufruf der Funktion OnIdle
bewirken, während
sich die Anwendung im Leerlauf befindet?
3. Worin besteht der Unterschied zwischen einer OnIdle
-Task und einem Thread?
4. Wie heißen die vier Objekte zur Synchronisierung von Threads?
5. Warum sollten Sie keine höhere als die normale Priorität für die Threads in Ihrer Anwendung festlegen?
1. Wenn Sie die heutige Beispielanwendung ausführen und die Auslastung Ihres
Computers mit einem Systemmonitor überwachen, zeigt sich, daß die Prozessorauslastung
sogar ohne laufende Threads bei 100 Prozent bleibt. Die Funktion
OnIdle
wird fortlaufend aufgerufen, selbst wenn nichts zu tun ist.
Modifizieren Sie die Funktion OnIdle
, damit keine der OnIdle
-Tasks aktiv ist,
wenn es nichts zu tun gibt. Somit wird die Funktion OnIdle
erst dann wieder fortlaufend
aufgerufen, wenn einer der Threads aktiv wird. Dieser Zustand dauert nur
so lange an, bis beide Threads wieder ausgeschaltet sind. Der Prozessor kann
dann zu einer minimalen Auslastung zurückkehren.
2. Geben Sie beim Starten der unabhängigen Threads einem der Threads die Priorität
THREAD_PRIORITY_NORMAL
und dem anderen die Priorität THREAD_PRIORITY_LOWEST
.