Threads
Threads
Am weitesten entfernt von Clipper ist das Konzept der Threads. Während unter DOS quasi alle Programme (auch die speicherresidenten) sich einen einzigen "Thread" (auch CPU genannt) teilen mussten, ist es unter Windows so, dass ein Programm quasi in mehreren Threads (wörtlich "Fäden") laufen kann. Was Multitasking auf Betriebssystemebene ist, das ist Threading innerhalb eines Programms.
Im normalen, prozeduralen Ablauf erfolgt auf die Anforderung, eine Liste zu drucken, der Druck der Liste. Solange die Liste gedruckt wird, ist das Programm ausschliesslich mit der Liste beschäftigt, und alle anderen Anforderungen (Events) landen erst einmal in einer Warteschlange.
Mittels Threads lassen sich solche Aufgaben (Tasks) programmintern "auslagern" und werden dann quasi im Hintergrund abgewickelt.
Besonderheiten von Threads
Threads lassen sich von aussen kaum beeinflussen. Dazu gehört auch, dass mit Xbase++ Standard-Werkzeugen ein Thread nicht von aussen beendet werden kann. Mehr dazu in den Anwendungsbeispielen.
Jeder Thread hat seinen eigenen WorkSpace, d.h. Dateien, auf die zugegriffen werden soll, müssen in einem Thread (erneut) explizit geöffnet werden.
Bestimmte Einstellungen wie SOFTSEEK etc. sind immer nur thread-local.
Als STATIC deklarierte Variablen sind nicht thread-local, d.h. wenn ein Thread eine STATIC Variable verändert, greifen alle anderen Threads auf die eine, veränderte Variable zu.
Warum Threads?
Standardmässig schlägt das Herz eines Windows-Xbase-Programms im Event-Loop. Wenn eine Verarbeitung angestossen wird, die längere Zeit in Anspruch nimmt (wie früher das Erstellen eines Reports, oder heute eine Webanfrage, oder das Zusammensuchen einiger Datensätze aus einer riesigen Tabelle), dann "ruht" der Event-Loop, bis diese Verarbeitung beendet ist. Und das, obwohl in dieser Zeit andere Aufgaben abgewickelt werden könnten.
Der Einsatz von Threads kann also dafür sorgen, dass unsere Anwendungen deutlich benutzerfreundlicher werden.
Das Erzeugen eines Threads
Für das Erzeugen eines Threads wird die Funktion Thread() verwendet. Diese Funktion liefert ein Objekt der Thread-Klasse zurück.
oThread := Thread():new() oThread:start("GenerateDialogThread") oder oThread:start({|| GenerateDialogThread()})
Die Funktion Thread() liefert mit der Methode new() ein Objekt der Thread-Klasse zurück. Dieses Objekt selbst ist noch im Ruhe-Zustand. Erst durch die Methode start() wird der Thread aktiviert, indem die Funktion GenerateDialogThread aufgerufen wird. Analog zu Eval können in der Methode :start() Parameter an die aufzurufende Funktion übergeben werden.
Die Methode :start() erwartet entweder einen String (d.h. in diesem Fall darf die aufzurufende Funktion nicht als STATIC deklariert sein) oder einen Codeblock.
Kommunikation zwischen Threads
mittels der :synchronize() Methode
Die Methode :synchronize(), die übrigens auf ein Thread-Objekt angewandt wird, das den anderen Thread referenziert, wartet auf die Beendigung des anderen Threads und hat somit nur mit der Synchronisierung in Bezug auf das Thread-Ende zu tun.
Eine direkte Kommunikation zwischen zwei Threads, z.B. zur "Arbeitsteilung" ist damit nicht realisierbar.
mittels eines Signal()-Objekts
Die Signal()-Klasse stellt ein Verfahren zur Verfügung, um zwischen zwei (oder mehr) Threads zu kommunizieren.
Die betroffenen Threads müssen auf das gleiche Signal()-Objekt zugreifen können, daher muss dieses bei der Initiierung des zweiten (dritten, etc.) Threads als Parameter übergeben werden:
oThread := Thread():new() oSignal := Signal():new() oThread:start("MyThreadFunction", oSignal)
Auch hier kann nur darauf gewartet werden, ob der andere Thread reagiert oder nicht, eine feinere Steuerung ist von Hause aus nicht möglich.
Da beide Threads das gleiche Signal-Objekt verwenden, wäre denkbar, über die :cargo Instanzvariable Informationen/Anweisungen auszutauschen, oder eine Ableitung der Signal()-Klasse zu erzeugen, die weitere Kommunikationsparameter aufnehmen kann.
mittels eines User-Events
Hierzu verweise ich auf das zweite Beispiel Browse und Edit in verschiedenen Threads.
Ablauf von Threads
Bis hierhin erscheint es so, dass Threads einmal aufgerufen werden, einen Task ausführen und wieder enden. Das ist nur ein Aspekt dessen, was möglich ist.
Denkbar ist, dass ein Thread während der ganzen Programmlaufzeit seinen eigenen Event-Loop abarbeitet und auf eine bestimmte Art von Ereignis wartet und darauf reagiert, während der Anwender mit dem Programm andere Abläufe bearbeitet.
Über die Methode :setInterval(<nHSec>) wird das Laufzeitsystem angewiesen, alle nHSec 1/100 Sekunde den Thread neu zu starten. Der Neustart des Threads geschieht jedoch frühestens, wenn der aktuelle Code abgearbeitet ist und der Thread beendet ist.
Besonderheiten bei den Methoden
Die Thread-Klasse weist PROTECTED Methoden auf, die nicht verwendet werden können:
- :atStart()
- :atEnd()
- :execute()
Auf den ersten Blick macht es keinen Sinn, solche Methoden zu definieren. Der Hintergedanke ist jedoch, dass der Programmierer eigene Thread-Klassen ableiten kann, in denen diese Methode dann verwendbar sind.
Die :atStart() Methode wird beim Start, vor dem Aufruf des eigentlichen Thread-Codes, ausgeführt. Analog wird die :atEnd() Methode nach dem Ende des Threads aufgerufen.
Anwendungsbeispiele
Anzeige der Uhrzeit
Das klassische Beispiel wäre die Darstellung einer Uhrzeitanzeige im Programm (das übernimmt inzwischen die Taskleiste). Vom Prinzip verdeutlicht diese Anwendung aber sehr gut, worum es bei Threads geht: es läuft eine Programmverarbeitung, die von anderen Programmteilen innerhalb des Programms logisch getrennt ist.
Browse und Edit in verschiedenen Threads
Die Standard-Programmierung von Browse und Edit stellt eine klassische Windows-Falle dar. Während "früher" das Browse-Fenster im Hintergrund blieb und erst nach Ende des Edit wieder aktiv wurde, kann der Benutzer heute zwischen den Fenstern wechseln (und erwartet diese Möglichkeit auch) und gegebenenfalls im Browse den Record-Pointer verschieben. Wenn nach Abschluss des Edit der Datensatz aktualisiert wird, geht die Änderung dann schon mal auf den falschen Satz, wenn man als Programmierer diesen Fall nicht berücksichtigt hat.
Laufen Browse und Edit in unterschiedlichen Threads, so hat jeder Thread einen eigenen Zugriff auf die Datei, und wenn im Browse-Fenster der Record-Pointer verschoben wird, ändert sich im Edit-Thread nichts.
Dies stellt einen Gewinn dar, eröffnet aber ein anderes Problem: wenn der Anwender im Edit-Thread Daten verändert hat, wie erklärt man dem Browse, dass aktuellere Daten anzuzeigen sind?
Ein genauerer Blick in die AppEvent.ch zeigt, dass es eine Klasse von User-Events gibt, die mit dem Wert xbeP_User beginnen.
Es empfiehlt sich, eine "AppEventUser.ch" zu erstellen, in der man eigene User-Events definiert, wie z.B.
#DEFINE xbeP_USync xbeP_User + 1
Damit zwei Threads miteinander kommunizieren können, muss der Thread, der eine Information senden will, Zugriff auf ein Objekt des anderen Threads haben.
FUNCTION BrowseEditStart(oBro, nRecord) Local oThread oThread := Thread():new() oThread:start("BrowseEditStartThread", oBro, nRecord) RETURN(.T.)
Die Methode :start() nimmt - analog zu Eval() auch Parameter entgegen, die an den neuen Thread weitergeleitet werden sollen. In diesem Beispiel ist oBro eine Referenz auf das XbpBrowse()-Objekt, aus dem der Edit-Thread aufgerufen wird. nRecord ist ein Hinweis, welcher Datensatz bearbeitet werden soll (es kann auch ein eindeutiger Schlüssel übergeben werden).
Wenn in BrowseEditStartThread() eine Änderung vorgenommen wird, muss ein entsprechendes Event in dem Thread erzeugt werden, in dem oBro verarbeitet wird. Dies geschieht durch PostAppEvent():
PostAppEvent(xbeP_USync, NIL, NIL, oBro)
Das Event wird an den Event-Loop des Threads gesendet, in dessen Kontext oBro erzeugt wurde.
Der Event-Loop im Browse-Thread sieht etwa so aus:
WHILE nEvent <> xbeP_Close nEvent := AppEvent(@mp1, @mp2, @oXbp) DO CASE CASE nEvent = xbeP_Keyboard ... CASE nEvent > xbeP_User DO CASE CASE nEvent = xbeP_USync oXbp:RefreshBrowse() OTHERWISE oXbp:handleEvent(nEvent, mp1, mp2) ENDCASE OTHERWISE oXbp:handleEvent(nEvent, mp1, mp2) ENDCASE END
Auf diese Weise kann durch die Art der generierten User-Events ein entsprechendes Verhalten in einem anderen Thread herbeigeführt werden.