HSG |
|
Grundlagen zu Threads
Man verwende ein einfaches Programm zur akustischen Aussendung eines Morse-Strings. Während der String gesendet wird (es piepst!), versuche man das Programm-Fenster zu verschieben oder für den nächsten Durchgang schon mal eine neue Dit-Zeit einzugeben. Das geht genauso wenig wie ein Beenden des Programms. Das Programm scheint während des Aussendens des Morse-Strings blockiert zu sein.
Das oben erwähnte Programm verwendet eine Klasse TMorseSender, die die Aussendung des Morse-Strings übernimmt. Es ist sicher nützlich, den Aufbau dieser Klasse zu verstehen, bevor man die Erweiterung zum Thread angehen kann.
Beim Inspizieren das Quelltextes stellt man fest, dass TMorseSender keine besonderen Eigenschaften erbt, denn hinter dem Schlüsselwort class ist nichts angeführt. Dass ist bekanntlich gleichbedeutend mit class(tObject). Wir ändern das zu class(TThread), das heißt, wir wollen alle Attribute und Methoden einer - anscheinend vordefinierten - Klasse TThread erben.
type
TMorseSender = class(TThread)
Geht man evolutionär vor, so erhält man schnell eine Fehlermeldung, dass Delphi TThread nicht kennt. Das läßt sich aber nach bewährter Manier flicken:
uses
windows, classes;
Jedes Thread-Objekt enthält die (parameterlose!) Methode execute, die den eigentlichen Thread darstellt. Man unterscheide genau zwischen dem Thread-Objekt zur Verwaltung und Steuerung und dem 'kleinen Prozess', dem Thread, der das ausführt, was in execute steht. Die Methode execute ist bereits vorhanden, muss aber überschrieben werden, wenn man eigenen Code unterbringen will.
public
....
procedure execute; override;
Die Implementierung wurde von procedure TMorseSender.sende(mstr: string); übernommen. Man sieht, dass die Methode sende den Parameter mstr hat, in dem der zu sendende String übergeben wird. Execute muss aber parameterlos sein. Die Lösung besteht darin, Parameter - hier: MorseString - aus der Klasse selbst zu verwenden.
procedure TMorseSender.execute; var i,n : integer; begin n := Length(MorseString); for i := 1 to n do begin case MorseString[i] of '.' : begin windows.Beep(frequenz,dit); sleep(dit); end; '-' : begin windows.Beep(frequenz,3*dit); sleep(dit); end; ' ' : begin sleep(dit); end; '/' : begin sleep(6*dit); end; end; // of case end; end;
private
dit : integer;
frequenz : integer;
morseString : string;
....
Bevor der Thread loslaufen kann, müssen im Allgemeinen Parameter über Attribute gesetzt
werden. Daher wird beim Erzeugen des Thread-Objekts der Thread zunächst ausgesetzt
(suspended). Das geschieht im Konstruktor durch die Übergabe des Parameters
CreateSuspended als true. Oft wird nach Ablauf eines Threads das Thread-Objekt
entbehrlich sein. Das Setzen des Attributs FreeOnTerminate auf true sorgt für eine
automatische Freigabe des Thread-Objekts nach Beendigung des Threads.
Gibt sich der Thread selbst frei, so wird nicht der Destructor destroy aufgerufen.
Eventuelle Aufräumarbeiten sollen von der Ereignisbehandlung zu OnTerminate erledigt
werden. Das Ereignis OnTerminate tritt direkt vor der Freigabe auf.
constructor TMorseSender.create;
begin
inherited create(true);
FreeOnTerminate := true;
OnTerminate := cleanup;
dit := 100;
frequenz := 2000;
end;
procedure TMorseSender.cleanup(Sender : TObject);
begin
// hier wird aufgeräumt, z.B. Destruktoren aufgerufen
end;
In der Vorlage wurde das MorseSender-Objekt vom Formular in OnCreate erzeugt und
in OnDestroy wieder freigegeben. Da sich der (ein) Thread selbst freigibt, ist die Freigabe
in OnDestroy zu löschen, das damit entbehrlich wird. Das Erzeugen, 'Attributieren' und
Starten (Resume) eines Threads wird man einem Button 'sende' übertragen.
Man beachte, dass es durchaus möglich - wenn auch im vorliegenden Fall wenig sinnvoll - ist,
mehrere Threads verschränkt zu starten.
procedure TForm1.bSendeClick(Sender: TObject);
begin
mSender := TMorseSender.create;
mSender.SetDit(StrToInt(eDit.Text));
mSender.SetMorseString(eSende.Text);
mSender.Resume;
end;
Es soll nicht verschwiegen werden, dass im vorliegenden Beispiel der Thread seine Ausgabe nur über das - thread-sichere - windows.beep macht. Thread-sicher heißt in diesem Zusammenhang, verschiedene Threads können die Methode ohne Einbau besonderer Sicherungen benutzen. Die Methode wird von einem benutzenden Thread in jedem Fall ordnungsgemäß und ungestört zu Ende geführt. Wir sind zuversichtlich, dass auch die Methoden von TNetzHW thread-sicher sind.
Baue analog zu obigem Beispiel das Programm MorseSender1 auf MorseSender1Thread um und teste es.
thread0.zip thread1.zip thread0mk.zip
Buchstaben_ausgeben_sleep2.zip
procedure TForm1.bAusgebenClick(Sender: TObject); var i,n : integer; s : string; begin s := eEin.Text; n := Length(s); for i := 1 to n do begin mAus.Text := mAus.Text+s[i]; refresh; // erzwingt Erneuerung der Ausgabe sleep(300); end; {i := 1; while i<=n do begin mAus.Text := mAus.Text+s[i]; Inc(i); sleep(300); end;} end;
Im 'guten alten Turbo-Pascal' gab es die Prozedur delay , mit der Hilfe man im Programm einstellbar warten konnte. Die Prozedur sleep scheint hier ein würdiger Nachfolger zu sein. Das Erste, was man bemerkt, ist, dass sich scheinbar nichts tut. Erst nach Abarbeitung der ganzen Schleife wird die Bildschirmanzeige aktualisiert. Daran ändert auch der Ersatz der for- durch eine while-Schleife nichts. Die Methode refresh hingegen erzwingt eine Aktualisierung des Fensters. Ruft man obiges Programm mehrfach auf, so stellt man erfreut fest, dass sie scheinbar parallel korrekt arbeiten. Anders wird es, wenn man die Ausgabe im gleichen Programm mehrfach parallel haben möchte. Das funktioniert mit sleep nicht! Erst wenn der erste 'Prozess' abgearbeitet ist, wird der zweite in Angriff genommen.
Buchstaben_ausgeben_timer2.zip
.... private i,n,i2,n2 : integer; s,s2 : string; .... procedure TForm1.bAusgebenClick(Sender: TObject); begin s := eEin.Text; n := Length(s); i := 1; Timer1.Interval := 10; Timer1.Enabled := true; end; procedure TForm1.Timer1Timer(Sender: TObject); begin if i<=n then begin mAus.Text := mAus.Text+s[i]; if i<n then begin Timer1.Interval := 300; Inc(i); end else Timer1.Enabled := false; end; end;
Eine zunächst aufwändiger erscheinende Methode, das Gleiche zu erreichen, ist die Verwendung von Timern. Hier wird das 'eigentliche Handeln' von der Ereignisbehandlungsprozedur eines Timers übernommen, die nach Ablauf gestartet wird. Durch Verändern der Attribute enabled und Interval des Timers wird der Ablauf gesteuert. Man hat die Hoffnung, dass ein Timer während er abläuft keine oder nur wenig CPU-Zeit blockiert. Und wirklich, wie der Versuch zeigt, scheinen die beiden 'Prozesse' im gleichen Programm ungestört parallel zu laufen. Es ist bei diesem Vorgehen zu bedenken, dass die Timer-Prozedur sich keine Daten vom letzten Aufruf her merkt. Hier muss man außerhalb speichern. Außerdem muss sich die Timer-Prozedur im Allgemeinen je nach äußeren Daten (Zuständen) unterschiedlich verhalten. Der erreichte Effekt scheint ein 'Multi-Threading' zu sein, ohne es ausdrücklich zu programmieren.