vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 3

Tag 18

Multitasking

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.

Heute lernen Sie, wie ...

Was ist Multitasking?

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.

Bei der 16/32-Bit-Struktur von Windows 95 ist es trotzdem noch möglich, daß eine sich schlecht verhaltende 16-Bit-Anwendung das System blockiert, da ein großer Teil von 16-Bit-Code weiterhin zum Kern des Betriebssystems gehört. Der 16-Bit-Code von Windows 95 stellt immer noch eine kooperative Multitasking-Umgebung dar, so daß nur jeweils eine Anwendung 16-Bit-Code zu einem bestimmten Zeitpunkt ausführen kann. Da alle USER- Funktionen und ein guter Teil der GDI-Funktionen auf die 16-Bit-Versionen zurückgreifen, kann eine einzige 16-Bit-Anwendung das gesamte System blockieren.

Wenn in Windows NT alle 16-Bit-Anwendungen in einem gemeinsam genutzten Speicherraum laufen, kann eine sich schlecht verhaltende Anwendung zwar alle 16-Bit-Anwendungen blockieren, was aber ohne Auswirkung auf irgendwelche 32-Bit-Anwendungen bleibt.

Mehrere Aufgaben gleichzeitig ausführen

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.

Threads zur Leerlaufverarbeitung

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.

Unabhängige Threads aufspannen

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.

Tabelle 18.1: Prioritäten von Threads

Priorität

Beschreibung

0

Der Thread erbt die Thread-Priorität der Anwendung, die den Thread erzeugt.

THREAD_PRIORITY_NORMAL

Eine normale (Standard-) Priorität.

THREAD_PRIORITY_ABOVE_NORMAL

1 Punkt über der normalen Priorität.

THREAD_PRIORITY_BELOW_NORMAL

1 Punkt unterhalb der normalen Priorität.

THREAD_PRIORITY_HIGHEST

2 Punkte über der normalen Priorität.

THREAD_PRIORITY_LOWEST

2 Punkte unterhalb der normalen Priorität.

THREAD_PRIORITY_IDLE

Prioritätsebene 1 für die meisten Threads (alle Threads ohne Echtzeitverarbeitung).

THREAD_PRIORITY_TIME_CRITICAL

Prioritätsebene 15 für die meisten Threads (alle Threads ohne Echtzeitverarbeitung).

Die Thread-Priorität steuert, wieviel CPU-Zeit der Thread in Relation zu den anderen Threads und Prozessen, die auf dem Computer laufen, erhält. Wenn ein Thread keinerlei Verarbeitungen ausführt, die schnell fertigzustellen sind, dann sollten Sie dem Thread beim Erstellen eine niedrigere Priorität geben. Es empfiehlt sich nicht, einem Thread eine Priorität höher als normal zu geben, solange es nicht unbedingt notwendig ist, daß der Thread seine Verarbeitung schneller als andere Prozesse auf dem Computer ausführen muß. Je höher die Priorität eines Threads ist, desto mehr CPU-Zeit erhält er und desto weniger CPU-Zeit steht für alle anderen Prozesse und Threads auf dem Computer zur Verfügung.

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.

Strukturen erstellen

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

Zugriff auf gemeinsam genutzte Ressourcen verwalten

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:

Kritische Abschnitte

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

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.

Semaphoren

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.

Ereignisse

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.

Tabelle 18.2: Member-Funktionen der Klasse CEvent

Funktion

Beschreibung

SetEvent

Versetzt das Ereignis in den signalisierten Zustand.

PulseEvent

Versetzt das Ereignis in den signalisierten Zustand und dann wieder zurück in den nicht signalisierten.

ResetEvent

Versetzt das Ereignis in den nicht signalisierten Zustand.

Unlock

Gibt das Ereignisobjekt frei.

Eine Multitasking-Anwendung erstellen

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.

Das Anwendungsgerüst

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.

Tabelle 18.3: Entwurf des Hauptfensters

Objekt

Eigenschaft

Einstellung

Kontrollkästchen

ID

Titel

IDC_CBONIDLE1

On &Idle Thread 1

Kontrollkästchen

ID

Titel

IDC_CBTHREAD1

Thread &1

Kontrollkästchen

ID

Titel

IDC_CBONIDLE2

On Idle &Thread 2

Kontrollkästchen

ID

Titel

IDC_CBTHREAD2

Thread &2

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.

Tabelle 18.4: Variablen der Steuerelemente

Objekt

Name

Kategorie

Typ

IDC_CBONIDLE1

m_bOnIdle1

Wert

BOOL

IDC_CBONIDLE2

m_bOnIdle2

Wert

BOOL

IDC_CBTHREAD1

m_bThread1

Wert

BOOL

IDC_CBTHREAD2

m_bThread2

Wert

BOOL

Fächer entwerfen

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.

Die Fächervariablen festlegen

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.

Tabelle 18.5: Die Variablen der Klasse CSpinner

Name

Typ

Beschreibung

m_crColor

int

Die aktuelle Farbe aus der Farbtabelle.

m_nMinute

int

Die Position bei der Drehung um die Achse.

m_iRadius

int

Der Radius (die Größe) des Fächers.

m_pCenter

CPoint

Der Mittelpunkt des Fächers.

m_crColors[8]

static COLORREF

Die Farbtabelle mit allen Farben, die im Farbfächer zu zeichnen sind.

m_pViewWnd

CWnd*

Ein Zeiger auf das Ansichtsobjekt.

m_bContinue

BOOL*

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: .

Den Fächer zeichnen

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.

Der Anfangspunkt der zu zeichnenden Linie wird für eine Position außerhalb des Mittelpunktes berechnet. Wenn der Ursprung für die Linien genau im Mittelpunkt des Fächers liegen soll, setzen Sie sowohl die x- als auch die y-Koordinate des Anfangspunktes auf 0.

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"

Die Fächer unterstützen

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: .

Die Fächerpositionen berechnen

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);

Die Fächer initialisieren

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.

Auch wenn Sie schon mehrere Beispiele zu Zeigern gesehen haben, verdient die Art und Weise der Übergabe eines Zeigers auf die Variable des Kontrollkästchens für den Fächer eine genauere Betrachtung:

m_cSpin[i].SetContinue(&((CTaskingView*)pView)->m_bThread1);

In dieser Anweisung nehmen Sie den Zeiger auf das Ansichtsobjekt pView, das einen Zeiger auf ein CView-Objekt darstellt, und führen eine Typumwandlung in einen Zeiger auf die spezielle Ansichtsklasse durch, die Sie in Ihrer Anwendung erstellt haben:

(CTaskingView*)pView

Da Sie nun den Zeiger auf das Ansichtsobjekt als CTaskingView-Objekt behandeln, können Sie zur Variablen m_bThread1 des Kontrollkästchens gelangen, die ein öffentliches Element der Klasse CTaskingView ist:

((CTaskingView*)pView)->m_bThread1

Nunmehr haben Sie Zugriff auf die Variable m_bThread1 und können die Adresse dieser Variablen ermitteln, indem Sie ein kaufmännisches Und-Zeichen vor die gesamte Anweisung stellen:

&((CTaskingView*)pView)->m_bThread1

Wenn Sie diese Adresse für die Variable m_bThread1 an die Funktion SetContinue übergeben, liefern Sie praktisch einen Zeiger auf die Variable m_bThread1. Damit kann man schließlich den Zeiger auf diese Variable, die das Fächerobjekt enthält, setzen.

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: }

Den Fächer drehen

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: }

Die OnIdle-Tasks hinzufügen

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.

Der Begriff Thread in den vorstehenden Erläuterungen ist etwas irreführend. Die Funktionalität, die Sie in die OnIdle-Funktion aufnehmen, läuft im Haupt-Thread der Anwendung ab. Die gesamte OnIdle-Verarbeitung, die Sie der Beispielanwendung hinzufügen, läuft nicht als unabhängiger Thread, sondern besteht eigentlich nur aus Funktionen, die sich über den Haupt-Thread aufrufen lassen.

Die OnIdle-Tasks starten und anhalten

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: }

Die OnIdle-Threads erstellen

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

Fortlaufend arbeitende OnIdle-Tasks

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

Unabhängige Threads hinzufügen

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.

Die Hauptfunktion der Threads

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: }

Die Threads starten und anhalten

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.

Die Threads vom Ansichtsobjekt auslösen

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.

Sauberes Herunterfahren

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.

Zusammenfassung

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.

Wenn Sie in Ihre Anwendungen die Fähigkeiten zum Multitasking einbauen wollen, sollten Sie sich darüber im Klaren sein, daß diese Aufgaben zu den komplizierteren Aspekten der Windows-Programmierung gehören. Sie müssen eine ganze Reihe von Faktoren verstehen und weit mehr in Betracht ziehen, als wir an einem einzigen Tag behandeln konnten. Wenn Sie Multitasking-Anwendungen erstellen möchten, sollten Sie sich mit einschlägigen Büchern zur Programmierung von Windows-Anwendungen mit MFC oder Visual C++ beschäftigen. Das Buch sollte profundes Wissen zum Multithreading mit MFC vermitteln und alle Synchronisierungsklassen detaillierter behandeln, als es an dieser Stelle möglich war. Denken Sie daran, daß Sie ein Buch brauchen, das sich auf MFC konzentriert und nicht auf die Entwicklungsumgebung von Visual C++. (MFC wird von den meisten kommerziellen C++-Entwicklungswerkzeugen für Windows-Anwendungen unterstützt. Dazu gehören auch die Compiler von Borland - jetzt Inprise - und Symantec. Eine Behandlung dieser Themen geht also über die Umgebung von Visual C++ hinaus.)

Fragen und Antworten

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: }

Workshop

Kontrollfragen

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?

Übungen

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 .



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbackKapitelanfangnächstes Kapitel


Ein Imprint des Markt&Technik Buch- und Software-Verlag GmbH.
Elektronische Fassung des Titels: Visual C++ 6 in 21 Tagen, ISBN: 3-8272-2035-1