Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Algorithmen, Datenstrukturen und Klassendesign (https://www.delphipraxis.net/78-algorithmen-datenstrukturen-und-klassendesign/)
-   -   Delphi OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Container? (https://www.delphipraxis.net/194082-omnithreadlibrary-ist-fuer-meine-anforderung-der-richtige-weg-welchen-container.html)

juergen 15. Okt 2017 19:51

OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Container?
 
Hallo zusammen,

ich habe ein Programm was Lied-Dateien einliest. Das Programm funktioniert. Aber inzwischen ist mir die Wartezeit beim Einlesen der Dateieigenschaften einfach zu lang. Am Anfang habe ich mich nach reiflicher Überlegung bewusst *gegen* eine Datenbank entschieden! Somit war klar, dass ich beim Starten des Programms die Dateien immer neu einlesen muss.
Das einlesen der Dateien über FindFirst() dauert bei ca. 15.000 Dateien akzeptable 1,7xxx Sekunden (mit Einfügen in ein Grid).
Allerdings dauert das Auslesen der Dateieigenschaften wie z.B. der MP3-Tags weitere 1:50 Minuten. An dem Auslesen der Dateieigenschaften kann ich nichts mehr optimieren.
Nun war meine Überlegung das Einlesen der Dateieigenschaften zu parallelisieren. Da ich noch Delphi XE nutze bin ich auf die OmniThreadLibrary gestoßen.

Nun meine Fragen (als absoluter Neuling in diesem Bereich):
1. Welche Funktionsart aus OmniThreadLibrary wäre für meine Anforderung richtig? So wie ich es verstanden habe könnte es Pipelines sein, bin mir aber nicht sicher.
2. Welchen (Threadsaved) Container soll ich für das Zwischenspeichern der Dateieigenschaften verwenden? Meine momentane Vorstellung ist, dass ich die Dateieigenschaften temporär in ein Container (ObjectList?) einlese.
3. Nach dem parallelen einlesen der Dateieigenschaften plane ich den Inhalt des "Containers" an ein Grid (cxGridTableView von DevExpress) weiterzugeben. Sind diese Überlegungen richtig/sinnvoll?

Ich danke schon mal im Voraus für eure Hilfe, Anregungen und Schubse in die richtige Richtung!

himitsu 15. Okt 2017 22:51

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Die Anzahl der Dateien ist fast irrelevant, schlimmer ist die Anzahl der Verzeichnisse, welche so gelesen wird. (also in Bezug auf die Dateisuche > FindFirst/FindNext)
Und dann nervt erst die Anzahl der Dateien, aber da du dort ja eigentlich kein großes Problem hast ... :angle:
Aber multithread kannst du beim Zugriff auf dein VCL-Grid vergessen.

Multithread auf einer HDD suchen verbessert auch nicht immer alles ... parallele Zugriffe können schnell mal alles extrem ausbremsen. (bei SSD sieht es anders aus)


Nicht alle Daten im Grid anzeigen/laden, sondern nur intern speichern, in einer Liste oder Tree,
oder ein schnelleres Grid verwenden (VirtualStringTree).



Du kannst auch weiterhin beim Start suchen und zur Laufzeit eine DB oder ein MemoryDataSet mit den Daten füllen und in einem DB-Grid anzeigen.
Delphi-Referenz durchsuchenTDataSet.Filter oder richtige SQL-Abfragen auf eine MemoryDB oder über LocalSQL von FireDAC ... da kannst du dann schöner in deinen Daten suchen.

Codehunter 16. Okt 2017 13:40

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Oder, etwas verrücktere Idee: Zur Visualisierung statt eines Grids einen VirtualTreeView verwenden und nur die Detaildaten der Dateien auslesen, die sich tatsächlich im Viewport befinden. Ob ein solches Vorgehen möglich ist, hängt natürlich stark von der jeweiligen Anwendung ab.

juergen 16. Okt 2017 18:47

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hallo,

mir ging es wirklich ums parallelisieren beim Einlesen der Dateieigenschaften. Das Grid ist in 1-2 Sek. mit allen Daten gefüllt. Nur beim parallelisieren des Ausleseprozesses der Dateieigenschaften sehe ich eine Möglichkeit die Wartezeit *spürbar* zu verringern. Doch worin sollte ich die Dateieigenschaften zwischenspeichern um diese nach dem parallelen Einlesen dem Grid zuzuordnen? Sollte ich ein Record verwenden, eine Klasse oder Objectlist oder eine generische Liste oder was gibt's noch? Das Ganze müsste halt Threadsaved sein.

Codehunter 17. Okt 2017 07:16

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von juergen (Beitrag 1383452)
Das Grid ist in 1-2 Sek. mit allen Daten gefüllt.

Darum ging es mir eigentlich gar nicht. Vielmehr als Denkanstoß, ob du die zig Tausend Dateien NUR einliest um sie AUSSCHLIESSLICH zu visualisieren. Oder tust du noch andere Dinge mit dem erhobenen Datenbestand? Falls es dir nur um Visualisierung geht, was ich aus der Aussage ableite dass du keine Datenbank verwenden willst, dann könntest du mit dem Virtualtree eine Menge Rechenzeit einsparen und ggf. ganz auf Multithreading verzichten.

Zitat:

Zitat von juergen (Beitrag 1383452)
Das Ganze müsste halt Threadsaved sein.

Wenn ich den Ablauf richtig verstanden habe, dann kennst du die genaue Anzahl von einzulesenden Dateien ja schon vorher. In dem Fall kannst du dir doch eine entsprechende Anzahl Records erstellen, welche die einzulesenden Daten aufnehmen, diese in eine TList packen und dem jeweiligen Thread nur einen Pointer auf den jeweiligen Record mitgeben. Synchronisieren müsstest du nur die Teile, die während des Einlesens schon benötigt werden. Der Rest liegt einfach erstmal als reservierter Speicher vor und wird nach und nach gefüllt.

Beim Datei-Explorer sieht man das sehr schön. Wenn man einen Ordner mit vielen Bildern hat und den Explorer auf Miniansichten stellt. Dann sieht man zunächst erstmal nur das Datei-Icon (anhand der Metadaten ausgewählt) und nachdem der Reader-Thread drüber marschiert ist, wird die Darstellung im Listview aktualisiert mit einem gerenderten Thumbnail (was wohl noch vom Reader-Thread erledigt wurde).

juergen 17. Okt 2017 22:26

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
@Codehunter ,
erst mal Danke für deine Tipps.
Ums visualisieren geht's mir gar nicht. Letztendlich geht es ums suchen von Interpreten, Titeln usw. um sich aus der gefilterten Menge schnell Playlisten zu erstellen.
Dazu muss ich eben alle Dateinamen, Dateipfade und Dateidatum einlesen. Das läuft schon über einen Thread und ist nicht das Problem.
Das Einlesen der Metadaten dauert dann aber zu lange. Diesen Prozess möchte ich beschleunigen durch Parallelisierung.
Nur wohin lese ich die Metadaten ein? Du nanntest hier eine TList. Wenn ich z.B. Interpret, Titel, Album, Bewertung Lyrics, Kommentar, Track#, Disk#, Dateidatum usw. pro Datei habe, sehe ich das Problem der Datentrennung. Wie soll ich die Metadaten einer Datei in einer TList speichern? Semikolon-getrennt? Dann ist das Einlesen der Daten in das Grid aufwendig. Schön wäre eigentlich eine InMemory-Table wie z.B. ClientDataSet oder dxMemTable von DevExpress. Die sind aber leider nicht Thread safe. Ein Record wäre auch eine Idee. Da finde ich aber nichts in meiner Delphi Hilfe ob der Thread safed ist. :gruebel:

Codehunter 19. Okt 2017 11:02

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zunächst einmal sei das hier als Einstiegslektüre empfohlen.

Prinzipiell ist dein Anwendungsfall einer der ganz einfachen Fälle, wo es kaum konkurrierende Zugriffe gibt. Leite dir einfach eine eigene Klasse von TThread ab, die du initial mit dem Pfad zur Zieldatei startest. Das Auslesen der Metadaten aus der Datei und dessen Zwischenspeicherung passiert ausschließlich innerhalb des Thread-Objektes. Da kannst du dich fast 1-zu-1 an dieses Beispiel halten. Wenn dein Thread mit seiner Arbeit fertig ist, übergibt er die gewonnenen Daten an den Hauptthread. Dafür dann die zuvor erwähnte Speicherreservierung.

Ein ungefähres Grundgerüst:
Delphi-Quellcode:
type
  Pmp3Data = ^Tmp3Data;
  Tmp3Data = record
    Interpret: string;
    Titel: string;
    Album: string;
    Bewertung: string;
    Lyrics: string;
    Kommentar: string;
    Track: Integer;
    Disk: Integer;
    Dateidatum: TDateTime;
  end;
 
  Tmp3DataList = TDictionary<string, Tmp3Data>;
 
  Tmp3ReaderThread = class;
 
  Tmp3ReaderComplete = procedure (Thread: Tmp3ReaderThread; FileName: string) of Object;
 
  Tmp3ReaderThread = class(TThread)
  private
    FData: Tmp3Data;
    FFileName: string;
    FOnComplete: Tmp3ReaderComplete;
  protected
    procedure DoComplete;
    procedure Execute; override;
  public
    property Data: Tmp3Data read FData;
    property FileName: string read FFileName write FFileName;
   
    property OnComplete: Tmp3ReaderComplete read FOnComplete write FOnComplete;
  end;

  Tmp3ThreadList = TList<Tmp3ReaderThread>;
 
  TForm1 = class(TForm)
    procedure Form1Create(Sender: TObject);
    procedure Form1Destroy(Sender: TObject);
  private
    DL: Tmp3DataList;
    TL: Tmp3ThreadList;
    FNumCompleteThreads: Integer;
    procedure AuslesenFertig(Thread: Tmp3ReaderThread; FileName: string);
  end;
 
const
  // Wie viele Threads sinnvoll sind, hängt von der CPU ab, also wie viele
  // Cores, ob Hyperthreading verfügbar ist oder nicht usw. Am besten
  // konfigurierbar machen.
  MAX_READER_THREADS_SAME_TIME: Integer = 8;

 
implementation
 
procedure TForm1.Form1Create(Sender: TObject);
begin
  DL:= Tmp3DataList.Create;
  TL:= Tmp3ThreadList.Create(TRUE);
end;

procedure TForm1.Form1Destroy(Sender: TObject);
begin
  FreeAndNil(DL); // !!! Achtung! Du musst die Records auch noch vorher mit Dispose freigeben !!!
  FreeAndNil(TL);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  Data: Pmp3Data;
  I, iFilesCount: Integer;
  sFile: string;
  SL: TStringList;
  T: Tmp3ReaderThread;
begin
  // ...
  Liste_alle_relevanten_Dateien_inkl_Pfad(SL);
  iFilesCount:= SL.Count;
 
  DL.Clear; // !!! Achtung! Du musst die Records auch noch vorher mit Dispose freigeben !!!
  TL.Clear; // Evtl. noch laufende frühere Threads werden abgebrochen
  FNumCompleteThreads:= 0;
  for I:= 0 to iFilesCount - 1 do begin
    sFile:= SL[I];
    New(Data); // Speicherplatz reservieren
    DL.Add(sFile, Data^);
    T:= Tmp3ReaderThread.Create(TRUE);
    T.FileName:= sFile;
    T.OnComplete:= AuslesenFertig;
    TL.Add(T);
  end;
 
  // Nun hast du eine Liste mit suspendierten Threads und eine Liste mit
  // reserviertem Platz für die Lese-Ergebnisse. Nun musst du nur noch dafür
  // sorgen, dass eine sinnvolle Anzahl Threads gleichzeitig läuft, z.B. acht.
 
  I:= 0;
  repeat
    T:= TL[I];
    T.Start;
    Inc(I);
  until (I = TL.Count) or (I = MAX_READER_THREADS_SAME_TIME);
end;

procedure TForm1.AuslesenFertig(Thread: Tmp3ReaderThread; FileName: string);
var
  Data: Pmp3Data;
  I: Integer;
  T: Tmp3ReaderThread;
begin
  Inc(FNumCompleteThreads);
  if DL.TryGetValue(FileName, Data) then begin
    // reservierten Speicher im Hauptthread mit den Daten aus dem Reader-Thread
    // füllen
    Data^.Interpret = Thread.Data.Interpret;
    // usw. ...
   
    I:= 0;
    repeat
      // Threadliste nach dem nächsten noch nicht abgearbeiteten Thread
      // durchsuchen und den Thread starten.
      T:= TL[I];
      if not T.Finished then begin
        T.Start;
        Break;
      end;
      Inc(I);
    until I = TL.Count;
  end;
end;

procedure Tmp3ReaderThread.Execute;
begin
  Lese_die_Datei_aus(FFileName, FData); // Speicher von FData liegt im Thread!
 
  DoComplete;
end;

procedure Tmp3ReaderThread.DoComplete;
begin
  if Assigned(FOnComplete) then begin
    Synchronize(FOnComplete(Self, FFileName));
  end;
end;
Ich habs in Notepad++ geschrieben, ungetestet! Daher nicht wundern wenns irgendwo kleinere Tippfehler gibt. Soll ja auch nur ein Denkanstoß sein. Das Dictionary-Object DL enthält am Ende eine Liste von Records, welche sich über den Dateinamen als Schlüssel abfragen lassen. Weil die vom Thread ausgelesenen Metadaten auch nur im Thread selbst existieren, brauchst du dir um Threadsafe an der Stelle keine Gedanken machen. Das eigentliche Auslesen der Metadaten verwendet dann evtl. wieder Routinen, wo du darauf achten musst. Aber das soll ein anderes Thema sein.

Anstelle der Records kannst du auch eine Klasse bauen. Wie du die Auslese-Ergebnisse verwaltest ist eigentlich dir überlassen. Ich verwende gerne Records, weil ich den Speicher vorher in der benötigten Menge reservieren kann. Klassen haben dann wieder den Vorteil, dass sich der Delphi-Speichermanager um das saubere Freigeben kümmert.

Nachdem das Auslesen fertig ist, wird
Delphi-Quellcode:
DoComplete
aufgerufen, welche den EventHandler
Delphi-Quellcode:
AuslesenFertig
im Hauptthread synchronisiert aufruft. Dort werden dann die ausgelesenen Daten aus dem Thread-Objekt in den Hauptthread übernommen.

Der Beispielcode ist absichtlich nicht komplett. Soll nur verdeutlichen, wie du den Speicher so verwalten kannst dass es bei der Threadverarbeitung nicht kracht. Die suspendierten Threads musst du in sinnvollen Häppchen starten. Die Anzahl ist von der Maschine abhängig und sollte nutzerkonfigurierbar sein.

Über FNumCompleteThreads hast du jederzeit im Hauptthread die Anzahl der bereits fertigen Threads, über TL.Count die Anzahl aller Threads inkl. der suspendierten und fertigen. Daraus kannst du ggf. noch eine Progressbar ansteuern. Aber WICHTIG: Diese Progressbar AUSSCHLIESSLICH über die Prozedur
Delphi-Quellcode:
AuslesenFertig
aktualisieren. Sonst gibts wieder Speicherkuddelmuddel.

freimatz 19. Okt 2017 14:07

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hi Leute,
wenn ich es richtig sehe geht es ihm nicht darum wie man das parallelisiert sondern, worin er das speichert.
Zitat:

Zitat von juergen (Beitrag 1383570)
Nur wohin lese ich die Metadaten ein? Du nanntest hier eine TList. Wenn ich z.B. Interpret, Titel, Album, Bewertung Lyrics, Kommentar, Track#, Disk#, Dateidatum usw. pro Datei habe, sehe ich das Problem der Datentrennung. Wie soll ich die Metadaten einer Datei in einer TList speichern? Semikolon-getrennt? Dann ist das Einlesen der Daten in das Grid aufwendig. Schön wäre eigentlich eine InMemory-Table wie z.B. ClientDataSet oder dxMemTable von DevExpress. Die sind aber leider nicht Thread safe. Ein Record wäre auch eine Idee. Da finde ich aber nichts in meiner Delphi Hilfe ob der Thread safed ist. :gruebel:


HolgerX 19. Okt 2017 15:12

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hmm..

Zitat:

Zitat von freimatz (Beitrag 1383683)
Hi Leute,
wenn ich es richtig sehe geht es ihm nicht darum wie man das parallelisiert sondern, worin er das speichert.
Zitat:

Zitat von juergen (Beitrag 1383570)
Nur wohin lese ich die Metadaten ein? Du nanntest hier eine TList. Wenn ich z.B. Interpret, Titel, Album, Bewertung Lyrics, Kommentar, Track#, Disk#, Dateidatum usw. pro Datei habe, sehe ich das Problem der Datentrennung. Wie soll ich die Metadaten einer Datei in einer TList speichern? Semikolon-getrennt? Dann ist das Einlesen der Daten in das Grid aufwendig. Schön wäre eigentlich eine InMemory-Table wie z.B. ClientDataSet oder dxMemTable von DevExpress. Die sind aber leider nicht Thread safe. Ein Record wäre auch eine Idee. Da finde ich aber nichts in meiner Delphi Hilfe ob der Thread safed ist. :gruebel:


Ist doch auch im Beispiel von Codehunter vorhanden:

je File ein Tmp3Data, gespeichert in DL: Tmp3DataList; als Liste... ;)

Wenn alle Threads fertig sind, kann auf DL zugegriffen werden (z.B. in AuslesenFertig) und die Metadaten aus der Liste als z.B. Tabelle angezeigt werden. ;)

juergen 19. Okt 2017 15:20

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hallo Codehunter,

erst einmal herzlichen Dank für deine ausführliche Unterstützung!! :thumb:
Ich glaube (:?) den groben Ansatz habe ich sogar verstanden. Da hab ich an den nächsten Schlecht-Wetter-Wochenenden einiges um mich einzuarbeiten.
Da die Art und Weise deiner Programmierung für mich neue Materie ist, werde ich wohl noch Nachfragen haben. :oops:

Also nochmals vielen Dank und einen schönen Tag noch!

GPRSNerd 20. Okt 2017 13:20

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hallo Codehunter,

ich habe interessiert mitgelesen, da ich ein ähnliches Problem mit dem Einlesen von Dateieigenschaften mit Parallelisierung lösen und beschleunigen möchte.
Ich habe deinen Code jetzt pro Forma mal in ein Testprogramm eingebaut und irgendwas stimmt mit der Parametrisierung des synchonize()-Aufrufs noch nicht:

[dcc32 Fehler] uMain.pas(247): E2250 Es gibt keine überladene Version von 'Synchronize', die man mit diesen Argumenten aufrufen kann

in der Codezeile:
Delphi-Quellcode:
    Synchronize(FOnComplete(Self, FFileName));


Hast du ne Idee? (dafür reicht mein Thread-Wissen nicht aus)

TiGü 20. Okt 2017 15:03

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von GPRSNerd (Beitrag 1383793)
[dcc32 Fehler] uMain.pas(247): E2250 Es gibt keine überladene Version von 'Synchronize', die man mit diesen Argumenten aufrufen kann

in der Codezeile:
Delphi-Quellcode:
    Synchronize(FOnComplete(Self, FFileName));
Hast du ne Idee? (dafür reicht mein Thread-Wissen nicht aus)


Wenn es in den Mainthread soll:
Delphi-Quellcode:
    Synchronize(nil, FOnComplete(Self, FFileName));


Wie machst du es sonst, wenn der Compiler nach dem Code eintippen meldet das in der Parameterliste ein Argument zuviel oder zuwenig ist?

GPRSNerd 20. Okt 2017 15:36

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von TiGü (Beitrag 1383795)
Wie machst du es sonst, wenn der Compiler nach dem Code eintippen meldet das in der Parameterliste ein Argument zuviel oder zuwenig ist?

Genau so vor meiner Anfrage gemacht und folgenden einzigen Parameter angezeigt bekommen:
Delphi-Quellcode:
AMethod

Nach dem Hinzufügen eines weiteren Parameters (zb nil) kommt immer noch die gleiche Fehlermeldung.
Es muss also eher an dem Parameter FOnComplete() liegen...

Delphi-Quellcode:
Synchronize(nil, FOnComplete(Self, FFileName));

TiGü 20. Okt 2017 15:46

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von GPRSNerd (Beitrag 1383797)
Zitat:

Zitat von TiGü (Beitrag 1383795)
Wie machst du es sonst, wenn der Compiler nach dem Code eintippen meldet das in der Parameterliste ein Argument zuviel oder zuwenig ist?

Genau so vor meiner Anfrage gemacht und folgenden einzigen Parameter angezeigt bekommen:
Delphi-Quellcode:
AMethod

Nach dem Hinzufügen eines weiteren Parameters (zb nil) kommt immer noch die gleiche Fehlermeldung.
Es muss also eher an dem Parameter FOnComplete() liegen...

Delphi-Quellcode:
Synchronize(nil, FOnComplete(Self, FFileName));

Abgesehen davon das der Cursor hinter der öffnen Klammer und Ctrl+Shift+Space vier Überladungen für Synchronize zeigt hast du natürlich recht.

Du musst den Code so anpassen:
Delphi-Quellcode:
procedure TMp3ReaderThread.DoComplete;
begin
  if Assigned(FOnComplete) then
  begin
    Synchronize(nil,
      procedure
      begin
        FOnComplete(Self, FFileName)
      end);
  end;
end;
Erklärung:
Es wird einer dieser Typen erwartet:
Delphi-Quellcode:
  TThreadMethod = procedure of object;
  TThreadProcedure = reference to procedure;
FOnComplete ist aber vom Typ
Delphi-Quellcode:
TMp3ReaderComplete = procedure (Thread: TMp3ReaderThread; FileName: string) of object;
, darum muss man da noch was drumstricken.

GPRSNerd 20. Okt 2017 16:37

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von TiGü (Beitrag 1383798)
Du musst den Code so anpassen:
Delphi-Quellcode:
procedure TMp3ReaderThread.DoComplete;
begin
  if Assigned(FOnComplete) then
  begin
    Synchronize(
      procedure
      begin
        FOnComplete(Self, FFileName)
      end);
  end;
end;

Compiled das bei dir? Ich sehe immer noch die gleiche Fehlermeldung.

TiGü 20. Okt 2017 17:05

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von GPRSNerd (Beitrag 1383801)
Zitat:

Zitat von TiGü (Beitrag 1383798)
Du musst den Code so anpassen:
Delphi-Quellcode:
procedure TMp3ReaderThread.DoComplete;
begin
  if Assigned(FOnComplete) then
  begin
    Synchronize(nil,
      procedure
      begin
        FOnComplete(Self, FFileName)
      end);
  end;
end;

Compiled das bei dir? Ich sehe immer noch die gleiche Fehlermeldung.

Ja.
Schreib mal TThread.Synchronize(nil,...

freimatz 20. Okt 2017 17:55

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von HolgerX (Beitrag 1383693)
Ist doch auch im Beispiel von Codehunter vorhanden:

je File ein Tmp3Data, gespeichert in DL: Tmp3DataList; als Liste... ;)

Ja das ist in dem Beispiel vorhanden. Ich sah das nur als ein Beispiel an um die Parallelierierung zu zeigen. Die konkreten Typen halte ich für diskussionswürdig und ich meinte darum ging es dem Threadersteller.

Nun konkret: Vorgeschlagen wurde in Record "Tmp3Data "für einen Eintrag. Das finde ich schon mal nicht schlecht.
Dann aber schreibst du "als Liste". Im Vorschlag steht aber "Tmp3DataList = TDictionary<string, Tmp3Data>;". Das ist keine Liste. :wink: TDictionary verwende ich selber häufig, juergen könnte dann Probleme haben alle Einträge da rauszuholen. Es kommt drauf an wie da zugriffen werden soll.
Später gibt es dann noch doch eine Liste "Tmp3ThreadList = TList<Tmp3ReaderThread>".
Ich möchte hier nicht dem Codehunter an den "Karren fahren", es sollte ja ein Denkanstoss sein und scheint auch geholfen zu haben. :)

GPRSNerd 20. Okt 2017 21:55

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von TiGü (Beitrag 1383805)
Ja.
Schreib mal TThread.Synchronize(nil,...

Nö, immer noch gleiche Fehlermeldung.

HolgerX 21. Okt 2017 00:13

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hmm..

Stell mal (bezogen auf das Beispiel) so um:

Delphi-Quellcode:
procedure Tmp3ReaderThread.Execute;
begin
  Lese_die_Datei_aus(FFileName, FData); // Speicher von FData liegt im Thread!
 
  Synchronize(DoComplete);
end;

procedure Tmp3ReaderThread.DoComplete;
begin
  if Assigned(FOnComplete) then
    FOnComplete(Self, FFileName);
end;

GPRSNerd 21. Okt 2017 12:16

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Jau, so compiled es jetzt!
Danke HolgerX.

Edit: Kleinere Korrekturen wären noch:
1. Der Konstruktor für die ThreadListe ist parameterlos: TL := Tmp3ThreadList.Create;
2. Das Auswählen des Records der DateiListe geht nicht mit Pointer, sondern (vermutlich) mit der Dereferenzierung: if DL.TryGetValue(Filename, Data^) then

Codehunter 23. Okt 2017 09:47

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Wie schon in meiner vorherigen Antwort geschrieben, hatte ich kein Delphi zur Hand und habe den Code "blind" in Notepad++ geschrieben. So wie er in meinem Kopf funktionierte ^^ Dass es da kleinere Unzulänglichkeiten gibt ist ja ganz normal. Da mir konkrete Informationen zur Funktionsweise und Zweck des Programms fehlten, ist mein Beispiel recht generisch konstruiert und kann sicherlich verschlankt werden.

Prinzipiell finde ich Parallelprogrammierung in diesen Zeiten spannender denn je. AMD Ryzen Threadripper kommt mit 16C/32T daher und gefühlt 99,99% aller Delphi-Programme laufen Single-Threaded. Da dauert es nicht mehr lang und die Anwender beschweren sich über lausige Performance trotz Manycore-CPU.

Es gibt ganz tolle Programme, wo einem als Entwickler wirklich das Herz aufgeht wenn man genauer hinschaut und die Threads arbeiten sieht. ACDSee z.B. ist so ein Fall. Die Erzeugung von Thumbnails erfolgt mehrstufig und im Hintergrund. SmartFTP ist noch so ein Beispiel. Hier werden die einzelnen FTP-Loads parallelisiert und können sehr komfortabel im UI über ein SpinEdit auf mehr oder weniger Worker konfiguriert werden. Beides sind m.W. C++ Programme.

IMHO bräuchte es aber noch viel bessere Delphi-MT-Toolkits als bisher verfügbar sind. Denn wie dieses doch wirklich seeeeehr simple Beispiel zeigt, müssen wir immer noch erhebliche Verrenkungen machen um sowas zu parallelisieren.

A) die VCL ist in weiten Teilen immer noch Non-Threadsafe.

B) die RTL ist durch überladene Prozeduren zwar streckenweise Threadsafe, aber eben nicht durchgehend.

C) Die OH wird lästig: In gefühlt jedem zweiten Fall "Embarcadero Technologies verfügt zurzeit über keine zusätzlichen Informationen. Bitte unterstützen Sie uns bei der Dokumentation dieses Themas, indem Sie Ihre Kommentare auf der Diskussionsseite eingeben."

D) FMX wird noch lange Zeit kein Ersatz für die VCL sein, weil es einen großen Teil von Drittanbieter-Komponenten schlichtweg nicht für FMX gibt. Und selbst wenn ist das noch keine Garantie, dass das durchgehend Threadsafe ist.

E) Mir persönlich fehlt es nach wie vor an guten und allgemein verständlichen Büchern und Tutorials zum Thema. Es gibt ein paar schon recht betagte wie dieses hier wo ich nicht mal weiß von wem es ist. Zwar gut erklärt, geht aber nur ganz am Ende und sehr knapp auf die objektorientierte Threadprogrammierung ein.

Mein Fazit: Dringender Handlungsbedarf seitens Embadera.

GPRSNerd 24. Okt 2017 12:26

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hallo Codehunter,

no offense intended! Ich bin froh über deinen Beitrag!
Ich versuche nur, deinen Code zum Laufen zu kriegen, was ich auch gerade geschafft habe.

Folgende Verbesserungen habe ich noch eingebaut:

1. Start weiterer Threads in AuslesenFertig() mit der folgenden Kondition, da bereits gestartete Threads mit erneutem T.Start zu einer Exception führen:
Delphi-Quellcode:
if ((T.Suspended) and (not T.Finished) and (not T.Started)) then
begin
  T.Start;
  Break;
end;
2. Freigeben der Threads bereits bei Erzeugung konfigurieren:
Delphi-Quellcode:
T.FreeOnTerminate := True;


3. Freigeben der Pointer auf die Records in den Threads mit Dispose():
Delphi-Quellcode:
New(Daten); // Speicherplatz reservieren
try
  DL.Add(sFile, Daten^);
  T := TDateiReaderThread.Create(True); //Thread ist erstmal suspendiert
  T.FreeOnTerminate := True;
  T.FileName := sFile;
  T.OnComplete := DateiEinlesenFertig;
  TL.Add(T);
finally
  Dispose(Daten);
end;

Codehunter 24. Okt 2017 13:05

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von GPRSNerd (Beitrag 1384024)
no offense intended! Ich bin froh über deinen Beitrag!

Hab ich auch nicht so aufgefasst. Alles ganz entspannt. Mir war klar dass der Code nicht gleich laufen würde.

Zitat:

Zitat von GPRSNerd (Beitrag 1384024)
Ich versuche nur, deinen Code zum Laufen zu kriegen, was ich auch gerade geschafft habe.

Das freut mich.

Zitat:

Zitat von GPRSNerd (Beitrag 1384024)
Delphi-Quellcode:
if ((T.Suspended) and (not T.Finished) and (not T.Started)) then

Das finde ich seltsam. Nicht was du gemacht hast sondern weil
Delphi-Quellcode:
T.Suspended
und
Delphi-Quellcode:
not T.Started
nach meinem Verständnis eigentlich niemals beide gleichzeitig TRUE oder gleichzeitig FALSE sein sollten. Die Hilfe zu TThread.Started ist mal wieder außerordentlich hilfreich: "Embarcadero Technologies verfügt zurzeit über keine zusätzlichen Informationen. Bitte unterstützen Sie uns bei der Dokumentation dieses Themas, indem Sie Ihre Kommentare auf der Diskussionsseite eingeben."

Zitat:

Zitat von GPRSNerd (Beitrag 1384024)
Freigeben der Pointer auf die Records in den Threads mit Dispose()

Darauf hatte ich schon gleich im selben Post hingewiesen. Ein bisschen Spaß wollte ich euch ja auch noch übrig lassen... Wobei so wie du das machst, nicht ganz richtig ist. Aber das wirst schon noch merken ;-)

bytecook 24. Okt 2017 23:32

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Nochmal kurz was zu Omnithread

Omnithread hat für mich einige gewaltige Vorteile mitgebracht, insbesonders die Messageimplementierung, und der Fakt, dass alle Aktualisierungen der Gui mittels Messages im Mainthread erfolgen. Kein ausbremsendes Synchronize mehr, wenn man was visuell aktualisieren möchte.
Es braucht je nach Komplexität der Aufgabe definitiv ein paar Tage oder Wochen an Einarbeitungszeit, um effektiv Nutzen aus dem Library zu ziehen. Critical Sections kann man mit OTL in den meisten Fällen recht elegant umgehen, das spart in zeitintensiven Bereichen einige CS Kernelaufrufe.

Das Buch "Parallel Programming with OmniThreadLibrary" von Primož Gabrijelčič, erhältlich unter

https://leanpub.com/omnithreadlibrary

war und ist extrem hilfreich. Download als PDF, EPUB oder MOBI auf Leanpub verfügbar. (Btw, dort gibt es auch ein nettes Nick Bundle: https://leanpub.com/b/nicksdelphibookbundle/)

Den gesamten Buchinhalt kann man auch online lesen ... http://otl.17slon.com/book/chap00.html
Der für mich wichtigste Teil war der Lowlevel Teil ... http://otl.17slon.com/book/chap05.html#lowlevel
Zum besseren Verständnis des Messagings ... http://www.thedelphigeek.com/2008/07...s-otlcomm.html


Das Schöne bei OTL ist, daß man mit den OTLMessages Objekte, respektive Objektinstanzen in der TOmniValue verschicken kann.
Kommunikation zwischen Threads findet mittels TwoWayComm statt, Beispiele sind im Master von Omnithread immer mit an Bord. Vorsicht, die liegen nicht im \Examples, sondern im \Tests Verzeichnis.

PS: Falls ein Demo mit Tokyo nicht startet, einfach FastMM4 aus dem Demo-Projektquellcode auskommentieren...

Codehunter 25. Okt 2017 08:46

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
OTL muss ich mir unbedingt mal anschauen. Sieht vielversprechend aus. Aber dafür braucht es mehr Zeit als ich im Moment habe.

bytecook 25. Okt 2017 09:14

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von Codehunter (Beitrag 1384073)
OTL muss ich mir unbedingt mal anschauen. Sieht vielversprechend aus. Aber dafür braucht es mehr Zeit als ich im Moment habe.

Haha, ja, das kenn ich. Ich hab mir die Zeit quasi auch schmerzhaft aus meinen Projekten sowie Freizeit derb rausschneiden müssen... :cyclops:

MichaelT 25. Okt 2017 13:29

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Ohne die MP3Tags sprich einen Index darüber in einer Datei (DB) zu speichern wird die Sache nicht schnell. Eine relationale Desktop DB egal welcher Ausprägung wir zu langsam für einen Musikplayer.

Wenn du irgendwelche Datenbestände anhängst, dann muss man den Index sowieso immer neu aufbauen. In der Praxis baut der MP3 sammelnde Menschen nicht selbst ein Archiv auf, sondern irgendwer gibt ihm ein 'Medium'.

Du brauchst im Mittel zu lange für die Suche und das Laden. Unserer Erfahrung nach machen Benutzer nicht lange dabei mit, wenn sie ein Buchstaben tippen und es rührt sich nichts :).

Spätestens beim Suchen nach Titeln oder Teilen von Titeln bist du weg von der Schüssel.

Bei 'unserem' (nicht meinem im Sinne von meinem Eigentum) Musikplayer kam das Repertoire gleich mit mit manuell eingestellten Fadepunkten usw... Damit hat sich die Frage bezüglich dieses Repertoires nicht gestellt, aber beim Import und der Verwaltung für eine Enduser Version ohne Content.

Ich habe damals Indizes pro Directory gemacht und die konnte ich mergen zur Laufzeit.

Metadaten zu allen Lieder in einer ObjectList zu halten ist vermutlich etwas übertreiben.

15k Dateien ist für ein Musikarchiv eher das untere Limit. Das funktioniert für den Einzelnen, wenn du aber ein allgemeineres Archiv anschaust gehst du über 100k Titel.

Ich hoffe du willst das Programm nicht verkaufen. Den wenigsten ist bewusst, dass selbst wenn man die Lautstärke automatisch halbwegs normiert geht die Klangqualität genauso wie die natürliche Lautstärke verloren.

Zumal ich nicht genau weiß was das Programm macht ... deswegen meine Erläuterung aus der Sicht eines Musikplayers.

Zitat:

Zitat von juergen (Beitrag 1383357)
Hallo zusammen,
Allerdings dauert das Auslesen der Dateieigenschaften wie z.B. der MP3-Tags weitere 1:50 Minuten. An dem Auslesen der Dateieigenschaften kann ich nichts mehr optimieren.


Codehunter 25. Okt 2017 15:07

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
@MichaelT: Das sehe ich eigentlich ganz genauso. Der TE hat das Thema Datenbank ja von vornherein ausgeschlossen, weshalb ich mich auf eine Antwort zur konkreten Frage beschränkt habe. Prinzipiell ist ein multithreaded Datei-Auslesen ja nicht verkehrt. Was man dann aus den gewonnenen Daten macht, steht ja auf einem anderen Blatt.

juergen 25. Okt 2017 20:15

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hallo Codehunter,
hallo MichaelT,

leider habe ich noch keine Zeit gefunden mich mit Threads zu beschäftigen. Ich denke ich benötige dafür viel Zeit.

Zum Thema Datenbank vs. Daten (Musik-Tags) speichern in ein DevExpress-Grid (letztendlich eine Objectlist):
Es gibt einen sehr guten Player mit SQL-Lite-Datenbank -> MediaMonkey. Das ist quasi die eierlegende Wollmichsau in dem Bereich.

Warum hatte ich mich nun gegen eine Datenbank entschieden?
Das Problem sind die Änderungen an den Musikdateien. Auch der geniale MediaMonkey speichert nicht alle Änderungen in der Musikdatei selbst, sondern in seiner DB.
Weiterhin besteht das Problem, wenn man Musikdateien mit anderen Programmen ändert, dass dann *konsistent* zu synchronisieren *ohne jedes Mal die ganzen Dateien neu zu scannen*.
Aus diesem Grunde habe ich mich gegen eine DB entschieden. Ich speichere die Bilder und Tag-Änderungen *direkt* in den Musikdateien und brauch mich nicht um Synchronisierung kümmern.
Die Suche über das Grid ist vernachlässigbar, also schnell genug wie ich finde. Bei ca. 60.000 Dateien benötigt die Suche weniger wie 1 Sekunde.
Weiterer Vorteil OHNE Datenbank: Da alle Änderungen in den Dateien selbst gespeichert werden, ist der Wechsel zu einem anderen Programm unproblematisch.


Ich habe also nur das Problem des Zeitverhaltens beim Einlesen der Musik-Eigenschaften (Tags).


Aber auch hier habe ich mit Testprogrammen inzw. festgestellt, dass Parallelisierung nur zum Teil hilft. Der limitierende Faktor ist wohl die Festplatte! Wenn ich mit 8 Threads die Dateien einlese von einem (langsamen) NAS-Laufwerk ist das langsamer als mit 4 Threads. Bei meiner SSD sieht es besser aus.

OmniThreadLibrary scheint mir zum einarbeiten leichter zu sein, da es dazu ein Buch gibt und etliche Beispiele. Das werde ich aber erst sehen, wenn ich dazu komme mich mit dem Thema zu beschäftigen.

Codehunter 26. Okt 2017 07:19

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von juergen (Beitrag 1384156)
Aber auch hier habe ich mit Testprogrammen inzw. festgestellt, dass Parallelisierung nur zum Teil hilft. Der limitierende Faktor ist wohl die Festplatte! Wenn ich mit 8 Threads die Dateien einlese von einem (langsamen) NAS-Laufwerk ist das langsamer als mit 4 Threads. Bei meiner SSD sieht es besser aus.

Nun ja, alles andere hätte mich auch gewundert. Irgendeinen Grund muss es ja geben, dass SSDs so beliebt sind 8-) Es gibt immer limitierende Faktoren (Flaschenhälse). Bei einem NAS sind das noch viel mehr als bei einer lokal eingebauten Festplatte. Ich kenne genug Werbeversprechen mit großen Zahlen und wenn man genauer hinschaut ist alles nur heiße Luft. NAS mit GbE-Anbindung ist inzwischen Usus. Doch wie ist die Schnittstelle im System angebunden? Nicht selten findet man da noch 33 MHz PCI-Busse. Oder der verbaute SoC verdient nicht mal die Bezeichnung "CPU". Dann ist auch noch zu klären, ob die Netzwerkverbindung durchgehend Fullduplex läuft oder nicht - WLAN dazwischen: Schon nur noch Halbduplex.

Thumbs.db - schon mal gehört? Microsoft cached Miniaturansichten von Bilddateien auch in einer Datenbank. Aus den selben Gründen. Ich würde einen Cache in so ein Programm einbauen und zusätzlich eine Benutzerfunktion, welche die Daten in zwei Wegen synchronisiert: Vom Cache in die Dateitags oder umgekehrt. Welche Richtung bei Asynchronität bevorzugt werden soll, würde ich dem Anwender überlassen.

Grundsätzlich stellt sich die Frage: Entwickelst du nur für den Hausgebrauch oder marktorientiert?

juergen 26. Okt 2017 10:58

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Hallo Codehunter,

Zitat:

Zitat von Codehunter (Beitrag 1384180)
Grundsätzlich stellt sich die Frage: Entwickelst du nur für den Hausgebrauch oder marktorientiert?

Das Ganze ist ein rein privates Vergnügen und nur für mich und Freunde gedacht. Allerdings kann ich die jeweils neuen Erkenntnisse dann gut in der Praxis gebrauchen (für die Firma).
Ich programmiere grundsätzlich nur hobbymäßig :)
Klar, auch die Firma hat dadurch den Nutzen, da ich diverse Anforderungen dann umsetzen kann.

Codehunter 26. Okt 2017 11:36

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
So hab ich vor 20 Jahren auch mal angefangen :-) Nur wars bei mir ein Outlook-Express-Ersatz. Oder noch früher, eine Warenwirtschaft in Amiga Basic. (Rückblickend eine scheußliche IDE. Wer die nur hergestellt hat... ^^)

bytecook 26. Okt 2017 12:17

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von Codehunter (Beitrag 1384208)
* Oder noch früher, eine Warenwirtschaft in Amiga Basic. (Rückblickend eine scheußliche IDE. Wer die nur hergestellt hat... ^^) *

DAS waren noch Zeiten... wobei ... mein erster Programmierhobel war noch ein Texas Instruments 99/4A mit satten 12KB :P

Codehunter 26. Okt 2017 12:22

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Also der A500 war mein erster eigener. 1991 war das. Davor gabs an der Schule noch den KC-87/1.11. Der hatte schon fette 16 kB RAM und konnte die Hornhaut von den Fingern hobeln :-)

GPRSNerd 26. Okt 2017 20:24

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Nachdem die Threads jetzt so schön laufen, wollte ich meiner Applikation noch ein Feature (sprich Button) spendieren, um laufende Threads vollständig abzubrechen und keine neuen mehr zu starten.
Geht anscheinend nicht so, wie ich mir das gedacht habe: :oops:

Delphi-Quellcode:
  for I := 0 to TL.Count - 1 do
  begin
    T := TL[I];
    T.Terminate;
  end; -> Der Teil ist funktionslos, bricht also nix ab, die Threads laufen munter weiter, wahrscheinlich weil man nie den richtigen Zeitpunkt erwischt, bevor ein Thread an seinem Ende den nächsten noch suspendierten startet...
Delphi-Quellcode:
  DL.Clear;
  TL.Clear; -> Hier kommt es zu Exceptions, weil ich den laufenden Threads die Threadliste plattmache.
Ihr habt bestimmt ein paar gute Ideen, oder?

Codehunter 27. Okt 2017 06:22

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Globale boolesche Variable bauen, die im Mainthread vor dem Start des nächsten Threads abgefragt wird. Beim Start deines Threadstapels setzt du die auf FALSE und bei Klick auf deinen Stopbutton auf TRUE.

EDIT: Mir ist eben zufällig noch etwas sehr schönes in die Hände gefallen. Der Chef himself erklärt Threads. Sogar auf Deutsch, was für Einsteiger sicher kein Nachteil ist. Besonders spannend fand ich den Teil ganz am Ende mit den prozessübergreifenden Threads. Das wird ja inzwischen z.B. von Browsern verwendet, um jeden Tab in einem eigenen Prozess zu kapseln. Stürzt ein Prozess ab, weil ein schrottiges Javascript ihn ins Nirvana geschickt hat, dann nimmt es nicht gleich das ganze Browserfenster mit.

bytecook 27. Okt 2017 08:37

AW: OmniThreadLibrary: Was ist für meine Anforderung der richtige Weg? Welchen Contai
 
Zitat:

Zitat von GPRSNerd (Beitrag 1384292)
Nachdem die Threads jetzt so schön laufen, wollte ich meiner Applikation noch ein Feature (sprich Button) spendieren, um laufende Threads vollständig abzubrechen und keine neuen mehr zu starten.
Geht anscheinend nicht so, wie ich mir das gedacht habe: :oops:

Delphi-Quellcode:
  for I := 0 to TL.Count - 1 do
  begin
    T := TL[I];
    T.Terminate;
  end; -> Der Teil ist funktionslos, bricht also nix ab, die Threads laufen munter weiter, wahrscheinlich weil man nie den richtigen Zeitpunkt erwischt, bevor ein Thread an seinem Ende den nächsten noch suspendierten startet...
Delphi-Quellcode:
  DL.Clear;
  TL.Clear; -> Hier kommt es zu Exceptions, weil ich den laufenden Threads die Threadliste plattmache.
Ihr habt bestimmt ein paar gute Ideen, oder?

nach T.Terminate noch ein T.WaitFor verwenden? Waitfor kehrt erst nach Beendigung des Threads zurück.
http://docwiki.embarcadero.com/RADSt...f%C3%BChrt_ist
Oder Omnithread verwenden ...
Beispiele zum Terminieren von Tasks
http://otl.17slon.com/book/chap05.ht...ks-termination
Ich arbeite hauptsächlich mit Ableitungen von TOmniWorker in Verbindung mit IOmniTaskControl, intern arbeitet dort die Verwaltung mit Events (DSiWaitForTwoObjects),
die Worker werden mittels IOmniTaskControl.Terminate() sauber aufgelöst (.Terminate(1000) killt den Thread nach 1000ms, würde ich aber nicht machen). Wichtig für mich, da manche meiner Komponenten-Tasks weitere Tasks erzeugen...

@Codehunter ... das ist doch mal ein nettes Video - geb ich gleich mal an meinen Azubi weiter :)


Alle Zeitangaben in WEZ +1. Es ist jetzt 05:42 Uhr.

Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz