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
DoComplete
aufgerufen, welche den EventHandler
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
AuslesenFertig
aktualisieren. Sonst gibts wieder Speicherkuddelmuddel.