|
![]() |
Registriert seit: 3. Sep 2004 441 Beiträge Delphi 10.4 Sydney |
#1
Hallo,
ich stehe mal wieder von einem sehr abstrakten Problem. Mein Thread liest mir in Execute mehrere Werte aus und speicherte diese in den integer Variablen MeinThread.X und MeinThread.Y. Der Thread ist dann fertig und löst "OnTerminate" aus. Hier ein handgestricktes Beispiel:
Delphi-Quellcode:
Laut Delphi-Hilfe ist "OnTerminate" Threadsicher. Das bedeutet zunächst (nur), dass es im VCL/Hauptthread des Programms ausgeführt wird, und ich auf's GUI zugreifen darf, so wie hier mit ShowMessage etc.
procedure TForm1.MeinThreadTerminated(Sender: TObject);
var t: TMeinThread; begin if not assigned(Sender) then Exit; // Sicherstellen, dass der Sender wirklich unser erwarteter Thread ist if not (Sender is TMeinThread) then Exit; // Lokale Variable für sicheren Zugriff auf die Thread-Daten t := TMeinThread(Sender); ShowMessage(inttostr(t.x)); end; Es sagt mir jedoch nicht, ob ich "sicher" auf Thread-Interne Variablen zugreifen darf. In einem alten Thema hier im Forum -ich finde den Link gerade nicht, es ist viele Jahre her- ging es darum, dass ich bei einem Thread außerhalb von "Synchronize" niemals auf Thread-Variablen zugreifen darf, die in Execute verwendet werden. Selbst ein einzelne Boolean-Variable, die nur von .Execute verändert wird und vom VCL-Thread nur ausgelesen werden soll (bl = True = "Berechnung dauert an") soll, ist -so die damalige Aussage- "nicht threadsicher", weil Gott-weiß-was-passieren-kann, wenn der Thread gerade das Bit schreibt, und der VCL-Thread das Bit gleichzeitig ausliest. Was "Gott-weiß-was" denn überhaupt sein könnte, kann ich mir bis heute nicht erklären, aber seit dem hat sich bei mir eine sehr intensive Furcht vor abstrakten AccessViolations, dem plötzlichen ungültig-werden von Variablen und Objekten und was-nicht-alles etabliert, incl. abruptem Programmende und Bluescreen, was alles passieren kann, wenn ich dieses eine Bit aus dem Hauptthread lese, nachdem es irgendwann mal im Thread gesetzt wurde. Versteht mich nicht falsch: Ich verstehe die Notwendigkeit von Synchronisieren, CriticalSections, Race-Conditions und Niemals-auf-GUI-Elemente-zugreifen mittlerweile sehr gut. Es geht hier um dieses Abtrakte "Greif NIEMALS von außen auf Thread-Variabeln zu!", hier konkret: AUCH nach Thread Ende! Bei OnTerminate ist der Thread FERTIG. Es kann (meiner Meinung nach) zu keinen "Race Conditions" etc. mehr kommen. Wenn ich die Variablen X und Y des Threads garantiert niemals anfasse außer in .Execute und OnTerminate - da dürfte doch nichts passieren, oder? Im Gegensatz dazu habe ich in einem anderen Programmteil einen anderen Thread, der mehrere Statusinformationen hat, die fortlaufend im GUI aktualisiert werden sollen. Da muss ich natürlich(?) alle Variablen einmal "intern" (private) für den Thread haben, in den die Infos alle reinkommen, und in Synchronize(MeinSync) weise ich dann die Werte alle an die "public"-Variablen zu, so nach dem Motto
Delphi-Quellcode:
Muss ich jetzt für ein threadsicheres "OnTerminate" in Realfall all meine 20 Variablen von einer "private" in eine "public" mit Synchronize kopieren, oder kann ich da bedenkenlos drauf zugreifen?
procedure TMeinThread.MeinSync;
begin StatusX := FStatusX; StatusY := FStatusY; //etc. end; ChatGPT sagt dazu: Kann man in OnTerminate Threadsicher auf alle Variablen des Threads zugreifen?
Code:
Was ich für Quatsch halte, weil der Thread ja fertig ist, und die Variablen nicht mehr verändern kann(?).
ChatGPT 4o:
Nein, OnTerminate garantiert keine Thread-Sicherheit für den Zugriff auf die Variablen des Threads. Warum? OnTerminate läuft im Hauptthread, weil es von Synchronize aufgerufen wird. Der eigentliche Execute-Code des Threads läuft im Hintergrund-Thread. Falls der Hintergrund-Thread während des Zugriffs auf eine Variable noch arbeitet oder sie verändert, kann es zu Race Conditions oder Dateninkonsistenzen kommen. o3-mini-high sagt dazu:
Code:
Das würde ich auch eher denken?
Ja – unter bestimmten Voraussetzungen. Das OnTerminate-Ereignis wird nämlich im Hauptthread (über Synchronize oder PostMessage) aufgerufen, nachdem die Execute-Methode vollständig abgeschlossen ist. Das bedeutet, dass der Thread selbst nicht mehr aktiv seine Instanzvariablen ändert und du daher in OnTerminate auf deren Werte zugreifen kannst.
Wichtig ist dabei: Keine parallele Modifikation: Stelle sicher, dass keine anderen Threads (oder asynchrone Prozesse) gleichzeitig auf dieselben Variablen zugreifen. Normalerweise werden die Instanzvariablen des Threads nur von diesem selbst modifiziert, sodass das nach Beendigung sicher ist. Zeitpunkt: OnTerminate wird ausgeführt, bevor der Thread-Objekt tatsächlich freigegeben wird (bei FreeOnTerminate = True). Innerhalb von OnTerminate ist der Thread also noch vollständig gültig. Kurz gesagt: Solange der Thread nur seine eigenen Variablen verändert hat und diese nicht von anderen Threads parallel modifiziert werden, kannst du in OnTerminate threadsicher auf alle Instanzvariablen zugreifen.
Delphi 10.4 32-Bit auf Windows 10 Pro 64-Bit, ehem. Delphi 2010 32-Bit auf Windows 10 Pro 64-Bit
|
![]() |
Online
Registriert seit: 10. Jun 2003 Ort: Berlin 9.845 Beiträge Delphi 12 Athens |
#2
Ich halte wenig davon, für diese Zwecke OnTerminate zu verwenden. Viel sinnvoller ist es, wenn man dafür eigene Ereignisse oder Callbacks definiert. Erstens hat man dann die Kontrolle darüber wann was aus dem Thread herausgereicht wird, zweitens ist dann alles sauber benannt, drittens kann kein Zugriff versehentlich im falschen Threadkontext erfolgen (wenn man von außen auf interne Werte zugreifen kann) und viertens stellen sich dann solche Fragen gar nicht (weil man es selbst explizit macht).
Zudem kann sich die interne Implementierung ändern, wenn ein bestimmtes Verhalten nicht explizit dokumentiert ist. |
![]() |
Registriert seit: 3. Sep 2004 441 Beiträge Delphi 10.4 Sydney |
#3
Okay, das wäre dann identisch mit dem Verfahren, das ich bisher verwende, um aus laufenden Threads Informationen auszulesen.
Würde dieses Start vereinfachte Beispiel deinen Erwartungen entsprechen, oder habe ich was wichtiges vergessen oder falsch gemacht?
Delphi-Quellcode:
type
TBerechnungsThread = class(TThread) private InternX: Integer; // Interne Variable für die Berechnung procedure SyncX; // Synchronisiert InternX mit ExternX protected procedure Execute; override; public ExternX: Integer; // Externe Variable (thread-sicher über Synchronize) OnBerechnungFertig: TNotifyEvent; constructor Create; end; constructor TBerechnungsThread.Create; begin inherited Create(True); // Thread startet pausiert (Suspended=True) FreeOnTerminate := True; // Automatisch nach Beendigung freigeben OnBerechnungFertig := NIL; InternX := 0; SyncX; // auch ExtenX korrekt initialisieren, Synchronize brauchen wir hier noch nicht(?) end; procedure TBerechnungsThread.SyncX; begin FExternX := InternX; // Sicherer Transfer in den Hauptthread end; procedure TBerechnungsThread.Execute; begin // Beispielhafte Berechnung (hier einfach nur Zuweisung) InternX := Random(1000); // Zufallszahl als Beispiel // Synchronisiere den Wert von InternX zu ExternX Synchronize(SyncX); // Falls ein Ereignis registriert ist, rufe es im Hauptthread auf if Assigned(OnBerechnungFertig) then Synchronize(procedure begin OnBerechnungFertig(Self); end); end; // Hauptprogramm: procedure TForm1.BerechnungAbgeschlossen(Sender: TObject); begin ShowMessage('Berechnung abgeschlossen! ExternX = ' + IntToStr(TBerechnungsThread(Sender).ExternX)); end; procedure TForm1.StartThread; var Thread: TBerechnungsThread; begin Thread := TBerechnungsThread.Create; Thread.OnBerechnungFertig := BerechnungAbgeschlossen; // Ereignis setzen Thread.Start; // Thread starten end;
Delphi 10.4 32-Bit auf Windows 10 Pro 64-Bit, ehem. Delphi 2010 32-Bit auf Windows 10 Pro 64-Bit
|
![]() |
Online
Registriert seit: 10. Jun 2003 Ort: Berlin 9.845 Beiträge Delphi 12 Athens |
#4
Du machst dir das Leben unnötig schwer, indem du mit TNotifyEvent als Typ arbeitest.
Du kannst den Wert einfach in das Event direkt als Parameter hineinschreiben. Dann musst du nichts zwischenspeichern und brauchst nach außen auch keine Property. Ich kann das noch als Beispiel schreiben. Vorher aber eine Frage: Wie kompliziert ist die Berechnung eigentlich? Brauchst du wirklich eine eigene Klasse dafür oder hast du das genommen, weil du TThread nur als abgeleitete Klasse kennst? Sprich sind das nur z.B. 20 Zeilen oder ist das mehr? |
![]() |
Registriert seit: 3. Sep 2004 441 Beiträge Delphi 10.4 Sydney |
#5
Achja, das wäre natürlich auch eine einfachere Variant die Werte zurückzugeben. Ich nehme an, du meinst es beispielhaft so?
Delphi-Quellcode:
type
// Definition des Events mit allen relevanten Parametern TTestCompleteEvent = procedure(Sender: TObject; const ResultText: string; AllesOk: Boolean; ScreenWidth, ScreenHeight: Integer; ScreenDimensionMismatch, Vollbild: Boolean; CurrentMonitor: Integer) of object; TTestThread = class(TThread) private FOnTestComplete: TTestCompleteEvent; FResultText: string; FAllesOk: Boolean; procedure DoNotifyTestComplete; protected procedure InitializeVariabels; virtual; procedure Execute; override; public constructor Create; property OnTestComplete: TTestCompleteEvent read FOnTestComplete write FOnTestComplete; end; procedure TTestThread.DoNotifyTestComplete; begin if Assigned(FOnTestComplete) then // Hier mit Dummy-Werten FOnTestComplete(Self, FResultText, FAllesOk, 123, 456, False, True, 1); end; Im realen Programm für den Endkunden habe ich mehrere "Selbsttests", die beim Programmstart laufen sollen. Thematisch haben diese Selbsttests relativ wenig miteinander zu tun: Der eine holt den freien Festplattenspeicher, der andere überprüft in der Datenbank-Tabelle, ob ungültige Zeitbezüge vorhanden sind, der nächste ob in der Registry unerwartete Werte stehen etc. Aktuell läuft alles nacheinander, was sich im Laufe der Zeit immer weiter "aufschaukelt", und -je nach Netzwerkgeschwindigkeit- beim Programmstart zwischen 1-2 Sekunden bis zu mehreren Minuten brauchen kann, wenn der Kunde das Programm über eine Bambus-VPN-Leitung startet. Das kann so nicht weitergehen. Ich muss diese ganzen Tests in Threads packen. Das Programm kann/darf auch mit ungültigen Werten starten, die der Selbsttest eigentlich erst finden/beheben soll - das ist kein Problem, denn auch wenn sie beim Programmstart nach dem alten Verfahren hintereinander laufen, wird der Benutzer nur benachrichtigt. Die Idee ist also, dass alle Tests erzeugt werden, die Testroutine wird über den Thread laufen gelassen (Threadsicherheit bei Datenbankabfragen kommt nun hinzu, CoInitialize etc.!), und sobald fertig wird im GUI beim entsprechenden Menüpunkt ein grüner Haken oder ein rotes X angezeigt. Die Tests selbst müssen aber (je nach Test) Werte aus dem Thread anzeigen können, damit ich dem Benutzer auch zeigen kann, "was genau" nicht simmt. Wenn z.B. die Bildschirmauflösung sich von dem Wert in der Datenbank unterscheidet (es gibt eine Bedingung, wann das relevant ist), kommt eine Info an den Benutzer. Er kann nun die Auflösung in der Datenbank auf diesen (oder einen anderen) Wert ändern (neue Auflösung in die Datenbank schreiben; hat nix mit Thread zu tun), und den Test erneut laufen lassen. Da der Thread eh auf die DB zugreift, sagt er halt nicht nur "Auflösung richtig: Ja/Nein", sondern gibt mir auch die Auflösung, damit ich sie dem Benutzer im GUI anzeigen lassen kann, ohne sie *nochmals* auslesen zu lassen. Nach dem Ändern der Werte läuft der Thread erneut (Selbsttest), und beim OnTestComplete werden dann die Werte in den Edit-Feldern wieder aktualisiert. Damit kann erstmal das Programm starten, die Haken für "Ok" oder die X für "Warnung/Fehler" ploppen dann im Menü so nach und nach bei den einzelnen Punkten davon als Grafik auf. Zumindest reagiert das Program in dieser Zeit auf Benutzereingaben. --- Da mit Threads sehr viel Unglücke passieren können, die man beim Entwickeln nicht unmittelbar sieht (Arbeiten mit TAdoConnection ohne CoInitialize klappt meistens im DebugModus, und ohne Debugger in 70% der Fälle, aber nicht immer - für diesen und ähnliche Fehler benötigt man Erfahrung und Hintergrundwissen, was ich -in Bezug auf Threads- nicht soooo sicher habe, dass ich mich da jetzt munter in Gefilden bewegen Wollen würde, die dann weitere Probleme verursachen. > Brauchst du wirklich eine eigene Klasse dafür oder hast du das genommen, weil du TThread nur als abgeleitete Klasse kennst? Ich nehme an, du redest hier von anonymen Methoden/Threads? ChatGPT schlägt das hier vor, und das würde das Thema ja um den Faktor 100 vereinfachen:
Delphi-Quellcode:
Wäre das in etwa auch dein Vorschlag?
TThread.CreateAnonymousThread(
procedure var ResultText: string; AllesOk: Boolean; ScreenWidth, ScreenHeight: Integer; ScreenDimensionMismatch, Vollbild: Boolean; CurrentMonitor: Integer; begin // Beispielhafte Berechnung ScreenWidth := 1920; ScreenHeight := 1080; Vollbild := True; CurrentMonitor := 1; ScreenDimensionMismatch := (ScreenWidth <> Screen.Width) or (ScreenHeight <> Screen.Height); AllesOk := True; ResultText := 'Test abgeschlossen!'; // Synchronisieren mit Hauptthread und direkt Callback aufrufen TThread.Synchronize(nil, procedure begin TestAbgeschlossen(nil, ResultText, AllesOk, ScreenWidth, ScreenHeight, ScreenDimensionMismatch, Vollbild, CurrentMonitor); end); end ).Start;
Delphi 10.4 32-Bit auf Windows 10 Pro 64-Bit, ehem. Delphi 2010 32-Bit auf Windows 10 Pro 64-Bit
|
![]() |
Online
Registriert seit: 10. Jun 2003 Ort: Berlin 9.845 Beiträge Delphi 12 Athens |
#6
Ja, genau. Bei (bezogen auf den Code) kurzen Threadoperationen würde ich immer anonyme Threads (oder TTask oder ähnliches) nutzen. Die kann man irgendwo verpacken, aber da würde ich nicht extra eine Klasse ableiten. Das lenkt nur vom Wesentlichen ab.
Und auch bei deiner Methode DoNotifyTestComplete würde ich TThread.Synchronize mit einer anonymen Methode nutzen. Und wenn du mehrere Aufrufe benötigst, kannst du TThread.Queue verwenden, so dass der Threads weiterläuft, während der Aufruf an den Hauptthread geht. Aufpassen musst du, wenn du ganze Objekte auf diese Weise übergeben möchtest, da das ja nur Referenzen sind. Dann bietet es sich an, der Klasse ein Interface zu spendieren, damit dein Objekt lange genug lebt, aber dennoch freigegeben wird. Geändert von jaenicke (24. Feb 2025 um 18:09 Uhr) |
![]() |
Registriert seit: 3. Sep 2004 441 Beiträge Delphi 10.4 Sydney |
#7
Danke schonmal für den guten Input.
TTask kannte ich tatsächlich bisher nicht. Im Prinzip kann ich nun einfach meine alten Testprozeduren (fast!) 1:1 übernehmen, indem ich sie einfach mit TTask.Run umgebe. Klar, auch hier muss ich wieder auf die Threadsicherheit achten, aber der ganze Overhead entfällt tatsächlich komplett.
Delphi-Quellcode:
Nur noch eine Frage: ChatGPT hat (wahrscheinlich nicht ganz zu Unrecht) recht große Bedenken, dass "Self" bis zum Ende des Tasks nicht mehr existiert. Hier in dem Beispiel unwahrscheinlich, in der Praxis mit der Bambusleitung über VPN nicht zu 100% ausschließbar, dass der Benutzer das Programm wieder schließt, bevor der TTask beendet ist. ChatGPT will hier unbedingt Self in "SelfRef" zwischenspeichern, und dann auch unten SelfREF.ExternerWert := Wert; verwenden. Meiner Meinung nach rettet dies das Programm auch nicht mehr vor dem Absturz, wenn ich im Hauptprogramm schon die dazugehörige Komponente mit "FreeAndNil" freigegeben habe. Was tun? Einfach ExternerWert := Wert; (ohne Self oder SelfREF) zuweisen und das Beste hoffen, oder akuter Handlungsbedarf?
TTask.Run(
procedure var Wert: Integer; begin // Siehe Hinweis 1, hier im Beispiel ignorieren!!! SelfRef := Self; // Sicherstellen, dass wir auf die Instanz zugreifen können Wert := Random(100); TThread.Queue(nil, procedure begin ShowMessage('Ergebnis: ' + IntToStr(Wert)); // Konkret in meinem Beispiel: Self.ExternerWert := Wert; end); end); "Zur Güte" habe ich nun noch versucht, die Zuweisungen an Self über eine Callback-Prozedur zu lösen, damit ich nicht vom TTask aus (trotz TThread.Queue!) direkt auf die Komponenten-Variablen zugreifen muss. Unnötig? Dringend erforderlich? Ich bin auch sehr unglücklich, dass ich kein "Early Exit" mehr verwenden kann. Schließlich muss auch, wenn alle Vorbedingungen fehlschlagen, am Schluss der Wert ans Hauptprogramm mit TThread.Queue übergeben werden. Ich fände das jetzt relativ blöd und schwer zu warten bei jedem nicht bestandenen "if" mit " TestComplete(False, FUserName, FDomain, FVollbild); Exit;" das zu jeder Anweisung zu schreiben. Wäre aber wahrscheinlich dennoch sinnvoller als dieser Quatsch mit dem zwischengespeicherten Status "bl", der erst ganz am Schluss an "blOkay" übergeben wird.
Delphi-Quellcode:
PS: blOkay wird erst gegen Ende der Prozedur gesetzt, damit nicht das "bl=True" bei einer Exception als Rückgabewert gilt (blOkay wird also nur dann gesetzt, wenn alle Befehle ohne Exception ausgeführt werden, ansonsten bleibt es bei blOkay=False als Initialisierungswert). Ist wie ich gerade sehe unnötig, weil ich bei except eh bei FAllesOkay ein statisches "False" als Parameter zum Testobjekt zurückgebe. Naja, egal.
procedure TSelbsthilfe_AutoLogon.Test;
const P = 'Test'; begin try TTask.Run(procedure var bl, blOkay: Boolean; tmpRegistry: TRegistry; FVollbild: Boolean; FResultText: string; FUserName, FDomain: string; begin tmpRegistry := NIL; try FUserName := ''; FDomain := ''; FResultText := ''; // Vollbildmodus auslesen FVollbild := HAL_Registry_GetBool('blFullScreen', False); if FVollbild then begin blOkay := False; tmpRegistry := TRegistry.Create(KEY_ALL_ACCESS OR KEY_WOW64_64KEY); try tmpRegistry.RootKey := HKEY_LOCAL_MACHINE; bl := tmpRegistry.OpenKeyReadOnly('Software\Microsoft\Windows NT\CurrentVersion\Winlogon'); if not bl then begin FResultText := _('Fehlgeschlagen: Status der automatische Anmeldung konnte ausgelesen werden.'); end; // AutoLogon-Option auslesen if bl and (not tmpRegistry.ValueExists('AutoAdminLogon')) then begin FResultText := _('Fehlgeschlagen: Automatische Anmeldung nicht aktiv: Wert existiert nicht!'); bl := False; end; if bl and (tmpRegistry.ReadString('AutoAdminLogon') <> '1') then begin FResultText := _('Fehlgeschlagen: Automatische Anmeldung nicht aktiv: Wert nicht wie erwartet!'); bl := False; end; // AutoLogon-Benutzername auslesen if bl and (not tmpRegistry.ValueExists('DefaultUserName')) then begin FResultText := _('Fehlgeschlagen: Automatische Anmeldung nicht aktiv: Benutzername existiert nicht!'); bl := False; end; if bl then begin FUserName := trim(tmpRegistry.ReadString('DefaultUserName')); if FUserName = '' then begin FResultText := _('Fehlgeschlagen: Automatische Anmeldung nicht aktiv: Benutzername ist leer!'); bl := False; end; end; // AutoLogon-Domain auslesen if bl and (not tmpRegistry.ValueExists('DefaultDomainName')) then begin FResultText := _('Fehlgeschlagen: Automatische Anmeldung nicht aktiv: Domain existiert nicht!'); bl := False; end; if bl then begin FDomain := tmpRegistry.ReadString('DefaultDomainName'); if FDomain = '' then begin FResultText := _('Fehlgeschlagen: Automatische Anmeldung nicht aktiv: Domain ist leer!'); bl := False; end; end; blOkay := bl; tmpRegistry.CloseKey; finally FreeAndNil(tmpRegistry); end; end else begin blOkay := True; FResultText := _('Erfolg: Dieser PC startet den Viewer nur im Vorschaumodus. Der Status der automatischen Windows-Anmeldung wurde nicht überprüft.'); end; TThread.Queue(nil, procedure begin TestComplete(blOkay, FUserName, FDomain, FVollbild); end); except on E: Exception do begin TThread.Queue(nil, procedure begin FResultText := Format(_('Fehlgeschlagen: Fehler beim Auslesen der automatischen Anmeldung: "%s".'), [E.Message]); TestComplete(False, FUserName, FDomain, FVollbild); end); end; end; end); except on E: Sysutils.Exception do begin LogP(QualifiedClassName, P, '', E.Message, ws_SEVERITY_EXCEPTION); end; end; end; procedure TSelbsthilfe_AutoLogon.TestComplete(_AllesOK: Boolean; _Username, _Domain: string; _Vollbild: Boolean); begin FAllesOk := _AllesOK; UserName := _Username; Domain := _Domain; Vollbild := _Vollbild; NotifyTestCompleted; if _Vollbild then begin Self.MyFrame.edtUserName.Text := _UserName; Self.MyFrame.edtDomain.Text := _Domain; end; end; Meine wichtigsten Fragen nochmal zusammengefasst: 1) TThread.Queue( ... Self.ExternerWert := Wert; ...) - Problematisch? Ja oder Nein? 2) Zusätzliche Referenz auf Self? Quatsch oder Notwendig? 3) TThread.Queue( ... TestComplete(False, FUserName, FDomain, FVollbild); ... ) NOTWENDIG oder DARF ich auch direkt innerhalb von TTask.Run
Delphi-Quellcode:
TThread.Queue(nil,
procedure begin FAllesOk := blOkay; UserName := FUsername; Domain := FDomain; Vollbild := FVollbild; NotifyTestCompleted; end; end);
Delphi 10.4 32-Bit auf Windows 10 Pro 64-Bit, ehem. Delphi 2010 32-Bit auf Windows 10 Pro 64-Bit
|
![]() |
Ansicht |
![]() |
![]() |
![]() |
ForumregelnEs ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.
BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus. Trackbacks are an
Pingbacks are an
Refbacks are aus
|
|
Nützliche Links |
Heutige Beiträge |
Sitemap |
Suchen |
Code-Library |
Wer ist online |
Alle Foren als gelesen markieren |
Gehe zu... |
LinkBack |
![]() |
![]() |