|
Antwort |
Registriert seit: 18. Mär 2005 1.682 Beiträge Delphi 2006 Enterprise |
#1
Hallo,
ich habe ein kleines Thread-Problem. Ich starte einen Thread, der eine Liste abarbeitet. Während er dies tut, kann es passieren, dass sich die Liste verändert. Beispielsweise wird die Liste geleert und mit neuen Inhalten gefüllt. Leeren, Füllen und Abarbeiten sind mit CirticalSections geschützt. Bei jedem Neubefüllen wird ein neuer Thread gestartet. Mein Problem ist folgendes: Angenommen, in der Liste sind 100 Elemente. Der Thread arbeitet sich durch die Liste, und nachdem er bei Position 50 angekommen ist, wird die Liste geleert und mit 100 neuen Elementen gefüllt. Der Thread bekommt vom Leeren und Neubefüllen nichts mit und macht mit (dem neuen) Element #51 weiter. Würde der Thread mitbekommen, dass die Liste zwischenzeitlich geleert wurde (eine Abfrage ist im Thread vorhanden), dann könnte sich der aktuelle Thread beenden, und der neu erzeugte arbeitet sich durch die neuen Elemente. Leider kommt der Thread aber zwischen Leeren und Neubefüllen nicht zum Zug. Folglich arbeitet der Thread bis zum Element 100, und danach kommt der neue Thread und fängt wieder bei Eins an. Ich würde also gerne zwischen Leeren und neu Befüllen meinem Thread etwas Zeit abgeben, damit er sich auf die neue Situation einstellen (bzw. sich beenden) kann. Etwa so:
Delphi-Quellcode:
Nur wie sage ich dem Main-Thread, dass jetzt kurz ein anderer zum Zuge kommen soll?
Leere_Liste;
Suspend_MainThread; // <- Hier! Fülle_Liste; Gruß xaromz |
Zitat |
Registriert seit: 2. Mär 2004 5.508 Beiträge Delphi 5 Professional |
#2
Zitat von xaromz:
Bei jedem Neubefüllen wird ein neuer Thread gestartet.
Terminiere doch den alten Thread, ändere die Daten und starte einen Neuen. Der Thread muss ständig prüfen, ob er nicht schon terminiert wurde:
Delphi-Quellcode:
Der Thread sollte sich ausserdem beim Hauptthread(Anwendung) melden, wenn er mit der Arbeit fertig ist.
procedure TXXXThread.Execute;
begin for i:=0 to liste.Count-1 do begin if Termined then Exit; // <<<==== BearbeiteListenElement(i); end; end;
Andreas
|
Zitat |
Registriert seit: 18. Mär 2005 1.682 Beiträge Delphi 2006 Enterprise |
#3
Hallo,
Zitat von shmia:
Zitat von xaromz:
Bei jedem Neubefüllen wird ein neuer Thread gestartet.
Terminiere doch den alten Thread, ändere die Daten und starte einen Neuen. Der Thread muss ständig prüfen, ob er nicht schon terminiert wurde. ... Der Thread sollte sich ausserdem beim Hauptthread(Anwendung) melden, wenn er mit der Arbeit fertig ist. Übrigens weiß die Hauptanwendung gar nicht, dass es da einen Thread gibt . Gruß xaromz |
Zitat |
Registriert seit: 9. Jul 2003 Ort: Schwanewede 117 Beiträge Delphi 7 Enterprise |
#4
Hi.
Wenn du das leeren und Befüllen eh schon mit einer Semaphore geschützt hast, kann du dir an der Stelle doch einen Merker setzen, dass geändert worden ist. Der Thread setzt diesen Merker zurück, wenn er anfängt zu arbeiten und fragt ihn vor jedem Schleifenzugriff ab. Sitzt der Merker wieder, schießt sich der Thread selber ab ...
Delphi-Quellcode:
cu
procedure TXXXThread.Execute;
begin Geaendert:=FALSE; for i:=0 to liste.Count-1 do begin if Geaendert then Exit; BearbeiteListenElement(i); end; end; easywk
Björn
if all else fails - read the instructions |
Zitat |
Registriert seit: 18. Mär 2005 1.682 Beiträge Delphi 2006 Enterprise |
#5
Zitat von easywk:
Hi.
Wenn du das leeren und Befüllen eh schon mit einer Semaphore geschützt hast, kann du dir an der Stelle doch einen Merker setzen, dass geändert worden ist. Der Thread setzt diesen Merker zurück, wenn er anfängt zu arbeiten und fragt ihn vor jedem Schleifenzugriff ab. Sitzt der Merker wieder, schießt sich der Thread selber ab ...
Delphi-Quellcode:
cu
procedure TXXXThread.Execute;
begin Geaendert:=FALSE; for i:=0 to liste.Count-1 do begin if Geaendert then Exit; BearbeiteListenElement(i); end; end; easywk Gruß xaromz |
Zitat |
Registriert seit: 23. Apr 2006 19 Beiträge |
#6
Im Prinzip sollte die Abarbeitung so aussehen.
Delphi-Quellcode:
procedure TXXXThread.Execute;
var nIdx: integer; begin nIdx:= 0; repeat try cs.enter; if liste.Count >= nIdx then begin machwas mit liste[nidx]; inc(nIdx); end; // evtl. nIdx auf 0 setzen wenn liste.count < nIdx finally cs.leave; end; until (Idx >= liste.count) or (liste.count = 0) or Application.Terminate; end;
guste
|
Zitat |
Olli
(Gast)
n/a Beiträge |
#7
Aua aua aua ...
Von der ständigen Neuerstellung eines Threads kann ich nur abraten. Man sollte den einen Thread bei wiederkehrenden Aufgaben immer wiederverwenden. Und dann gehen wir nochmal ein wenig in's Detail in Sachen Threads. Von außen kann man den Thread niemals sicher an einer bestimmten Stelle anhalten [s.u.]. Das sinnvollste wäre also eine Kombination aus einer Liste/Listenklasse welche sich bewußt ist, daß mehrere Threads darauf zugreifen. Ich würde in jedem Fall eine verkettete Liste benutzen. Der Hauptthread sollte sich mit SuspendThread selbst schlafenlegen und nach dem Aufwachen checken ob die Liste noch gültig ist. Sinnvoll wäre da vermutlich so eine Art Ringpuffer aus mindestens 2 LIST_ENTRY Strukturen, wobei sich der Thread merkt auf welche Liste (0 oder 1) er gerade zugegriffen hat und dann nach dem Aufwachen erstmal testet, ob die Liste noch die gültige ist. Den Index der gültigen Liste würde ich threadsicher über die Interlocked*-Funktionen jeweils ändern und auch auslesen. Achtung, jede Liste braucht auch ein Ausschlußobjekt (Semaphore, Mutex oder Critical Section o.ä.). Das Ausschlußobjekt garantiert, daß die Liste solange weiterlebt, bis der (nun wieder aufgewachte Thread) es freigegeben hat. Sinnvoll wäre desweiteren, wenn der Thread in einer globalen Variablen (oder Member-Variablen) über die Interlocked-Funktion auch den Index des aktuell bearbeiteten Elements festhalten würde. Dann könnte der Thread welcher die Listen befüllt und auch wieder freigibt schonmal alle Elemente bis auf das fragliche wieder freigeben und würde zuguterletzt nur darauf warten dieses letzte Element freizugeben sobald das Ausschlußobjekt wieder frei ist. Nun nochmal zum Anhalten Wenn wir uns vor Augen führen, daß so ziemlich jeder "Delphi-Befehl" in viele (nicht-atomare) Opcodes (also CPU-Befehle) zerlegt wird, dann kann man sich leicht vorstellen, daß wir in "Delphi" folgendes haben (EIP = aktuelle Verarbeitungsstelle des Threads):
Code:
in "Assembler" sähe das dann z.B. so aus:
[color=red]Bla;[/color]
[color=green]Bla2(ListenElement);[/color] [color=blue]Bla3;[/color]
Code:
Die farblich gleichgestalteten Befehle entsprechen einander (HLL<>ASM).
[color=red]XOR ...
MOV ... OR ... MOV ... XCHG ... MOV ... AND ... TEST ... XOR ... MOV ... OR ...[/color] [color=green]MOV ... XCHG ... MOV ... AND ... TEST ... XOR ... MOV ... OR ... MOV ... [u][b]XCHG ...[/b][/u][EIP] MOV ... AND ... TEST ... OR ... MOV ... XCHG ... MOV ... AND ... TEST ... XOR ... MOV ... XOR ...[/color] [color=blue]MOV ... OR ... MOV ... XCHG ... MOV ... AND ... TEST ... XOR ... MOV ... OR ... MOV ... XCHG ... MOV ... AND ... TEST ...[/color] Aber holla, dadurch daß wir den Thread von außerhalb schlafengelegt haben, steht die Ausführung mitten in der Funktion die auf das Listenelement zugreift. Wird das Listenelement also freigegeben, kann dennoch ein Zugriff auf den entsprechenden (nun freigegebenen) Speicherbereich passieren. Mit etwas Glück kommt nur Müll raus, mit etwas Pech schießt sich der Thread selber ab, mit noch mehr Pech landet die Anwendung in den ewigen Jagd...ähem...Ausführungsgründen ... Das was guste geschrieben hat, ist einerseits nicht so dumm, andererseits naiv. Das Problem ist, daß man jedem Listenelement eine eigene Critical Section mitgeben müßte. Tut man's nicht, hat man sogleich den Sinn des Thread ad absurdum geführt ... Anwendung: Nehmen wir die Beispiele von oben, kackt dir der Thread in den meisten Fällen auch ab, wenn EIP mitten in der grün gefärbten Funktion stehenbleibt
Code:
(Geht bei shmia's Beispiel auch ...)
procedure TXXXThread.Execute;
begin Geaendert:=FALSE; for i:=0 to liste.Count-1 do begin if Geaendert then Exit; [color=green]BearbeiteListenElement(i);[/color] end; end; Tip: Teste dein Prorgramm auch auf einem SMP-System!!! Noch 'ne Idee: Wenn du eine zweifach verkettete Liste nimmst, kann der Thread ein Listenelement ausklinken und später wieder in die Liste einklinken. Wenn du dann immer mit Interlocked-Funktionen arbeitest, kann der "Füll-Thread" das gleiche machen und dein aktuell bearbeitetes Listenelement an den Anfang der Liste stellen. Kommt aber eben drauf an, ob deine Aufgabe zyklisch oder linear ist. |
Zitat |
Registriert seit: 18. Mär 2005 1.682 Beiträge Delphi 2006 Enterprise |
#8
Hallo,
Zitat von Olli:
Aua aua aua ...
Von der ständigen Neuerstellung eines Threads kann ich nur abraten. Man sollte den einen Thread bei wiederkehrenden Aufgaben immer wiederverwenden.
Zitat von Olli:
Und dann gehen wir nochmal ein wenig in's Detail in Sachen Threads. Von außen kann man den Thread niemals sicher an einer bestimmten Stelle anhalten [s.u.]. Das sinnvollste wäre also eine Kombination aus einer Liste/Listenklasse welche sich bewußt ist, daß mehrere Threads darauf zugreifen. Ich würde in jedem Fall eine verkettete Liste benutzen. Der Hauptthread sollte sich mit SuspendThread selbst schlafenlegen und nach dem Aufwachen checken ob die Liste noch gültig ist. Sinnvoll wäre da vermutlich so eine Art Ringpuffer aus mindestens 2 LIST_ENTRY Strukturen, wobei sich der Thread merkt auf welche Liste (0 oder 1) er gerade zugegriffen hat und dann nach dem Aufwachen erstmal testet, ob die Liste noch die gültige ist. Den Index der gültigen Liste würde ich threadsicher über die Interlocked*-Funktionen jeweils ändern und auch auslesen. Achtung, jede Liste braucht auch ein Ausschlußobjekt (Semaphore, Mutex oder Critical Section o.ä.). Das Ausschlußobjekt garantiert, daß die Liste solange weiterlebt, bis der (nun wieder aufgewachte Thread) es freigegeben hat.
Sinnvoll wäre desweiteren, wenn der Thread in einer globalen Variablen (oder Member-Variablen) über die Interlocked-Funktion auch den Index des aktuell bearbeiteten Elements festhalten würde. Dann könnte der Thread welcher die Listen befüllt und auch wieder freigibt schonmal alle Elemente bis auf das fragliche wieder freigeben und würde zuguterletzt nur darauf warten dieses letzte Element freizugeben sobald das Ausschlußobjekt wieder frei ist.
Zitat von Olli:
Nun nochmal zum Anhalten
Wenn wir uns vor Augen führen, daß so ziemlich jeder "Delphi-Befehl" in viele (nicht-atomare) Opcodes (also CPU-Befehle) zerlegt wird, dann kann man sich leicht vorstellen, daß wir in "Delphi" folgendes haben (EIP = aktuelle Verarbeitungsstelle des Threads): ... Die farblich gleichgestalteten Befehle entsprechen einander (HLL<>ASM).
Zitat von Olli:
Aber holla, dadurch daß wir den Thread von außerhalb schlafengelegt haben, steht die Ausführung mitten in der Funktion die auf das Listenelement zugreift. Wird das Listenelement also freigegeben, kann dennoch ein Zugriff auf den entsprechenden (nun freigegebenen) Speicherbereich passieren. Mit etwas Glück kommt nur Müll raus, mit etwas Pech schießt sich der Thread selber ab, mit noch mehr Pech landet die Anwendung in den ewigen Jagd...ähem...Ausführungsgründen ...
Zitat von Olli:
Das was guste geschrieben hat, ist einerseits nicht so dumm, andererseits naiv. Das Problem ist, daß man jedem Listenelement eine eigene Critical Section mitgeben müßte. Tut man's nicht, hat man sogleich den Sinn des Thread ad absurdum geführt ...
Zitat von Olli:
Anwendung:
Nehmen wir die Beispiele von oben, kackt dir der Thread in den meisten Fällen auch ab, wenn EIP mitten in der grün gefärbten Funktion stehenbleibt
Code:
(Geht bei shmia's Beispiel auch ...)
procedure TXXXThread.Execute;
begin Geaendert:=FALSE; for i:=0 to liste.Count-1 do begin if Geaendert then Exit; [color=green]BearbeiteListenElement(i);[/color] end; end;
Zitat von Olli:
Tip: Teste dein Prorgramm auch auf einem SMP-System!!!
Zitat von Olli:
Noch 'ne Idee: Wenn du eine zweifach verkettete Liste nimmst, kann der Thread ein Listenelement ausklinken und später wieder indie Liste einklinken. Wenn du dann immer mit Interlocked-Funktionen arbeitest, kann der "Füll-Thread" das gleiche machen und dein aktuell bearbeitetes Listenelement an den Anfang der Liste stellen. Kommt aber eben drauf an, ob deine Aufgabe zyklisch oder linear ist.
Da ich eine bestimmte Reihenfolge in der Liste habe und die Liste auch immer vollstängig sein muss, funktioniert das so leider nicht. Mein Problem hat sich aber jetzt verlagert (Siehe Posting #5): Wenn ich das Programm beende, während der Thread noch läuft, erhält dieser keine Chance, sich sauber zu beenden. Deshalb möchte ich eben beim Beenden des Programms den Hauptthread kurz schlafen legen, damit der Arbeitsthread auf ein Flag reagieren und sich beenden kann. Ich möchte also Windows sagen, dass jetzt erst mal alle anderen Threads ausgeführt werden sollen (meinetwegen auch ein bestimmter Thread, ich hab das Handle ja), und danach wieder der Hauptthread. Gruß xaromz |
Zitat |
Olli
(Gast)
n/a Beiträge |
#9
Zitat von xaromz:
Mach ich inzwischen. Der Originalcode ist durch Faulheit entstanden (eine Listenänderung kommt nicht so oft vor, deshalb ist normalerweise ah kein Thread aktiv).
Zitat von xaromz:
Mit dem Zugriff auf die Listenelemente hab ich kein Problem, der ist geschützt.
Zitat von xaromz:
Danke, ich spreche fließend x86er assembler .
Zitat von xaromz:
Wie gesagt, der Zugriff auf die Elemente ist gesichert.
Zitat von xaromz:
Siehe oben.
Zitat von xaromz:
Das wird so nicht funktionieren. Die Änderung der Liste wird nämlich währenddessen angezeigt. In der Liste befinden sich Objekte mit den Dateinamen von Mediendateien (mp3, avi, mpeg...). Die Namen stehen in einer Listbox. Im Hintergrund werden diese Objekte mit Metadaten der Medien gefüllt (Bitrate, Videoauflösung, Spurenanzahl, ID3-Tags...), und gegebenenfalls die Anzeige in der Listbox verändert.
Da ich eine bestimmte Reihenfolge in der Liste habe und die Liste auch immer vollstängig sein muss, funktioniert das so leider nicht.
Zitat von xaromz:
Wenn ich das Programm beende, während der Thread noch läuft, erhält dieser keine Chance, sich sauber zu beenden. Deshalb möchte ich eben beim Beenden des Programms den Hauptthread kurz schlafen legen, damit der Arbeitsthread auf ein Flag reagieren und sich beenden kann.
Meines Erachtens ist deine Denk- und Herangehensweise zu naiv. Du könntest dir eine Menge Arbeit sparen, wenn jede Liste ein Ausschlußobjekt hat (könnte ein Byte- oder DWORD-Wert sein), der immer nur über die Interlocked-Funktionen bearbeitet wird. Dieses Ausschlußobjekt ist kein echtes Objekt sondern ein Referenzzähler. Da jedes Objekt nun nur noch ein Pointer auf einen Speicherbereich ist, übergibst du dieses an einen Thread nachdem der Aufrufer den Referenzzähler um 1 erhöht hat. Danach kann der Thread erstmal machen was er will, außer daß er davon ausgehen muß, daß die Daten jederzeit von einem anderen Thread gelesen werden können - die Integrität muß also garantiert sein (z.B. kritischer Abschnitt für jede der zu schreibenden Informationen ...). Wenn der (Arbeits-)Thread mit der Abarbeitung fertig ist, legt er sich erstmal selber schlafen (suspend) nachdem er den Referenzzähler um 1 reduziert hat. Dadurch ist er immer an einer wohldefinierten Stelle eingeschlafen. Will der Dispatcher-Thread nun dem Arbeits-Thread sagen, daß er wieder was machen soll, wird der Pointer (globale Variable o.ä.) zum Listeneintrag atomisch ausgetauscht und der Arbeits-Thread wieder aufgeweckt, womit die Schleife im Arbeits-Thread ihre Arbeit weiterführt. Damit ist zwar ein extra Thread (der Dispatcher) nötig, aber die Anzahl der Arbeits-Threads kann ohne Mühe variiert werden. Außerdem mußt du nur einen Listeneintrag in der Listbox aktualisieren, nicht die komplette Liste.
Zitat von xaromz:
Ich möchte also Windows sagen, dass jetzt erst mal alle anderen Threads ausgeführt werden sollen (meinetwegen auch ein bestimmter Thread, ich hab das Handle ja), und danach wieder der Hauptthread.
|
Zitat |
Registriert seit: 18. Mär 2005 1.682 Beiträge Delphi 2006 Enterprise |
#10
Hallo,
Zitat von Olli:
Zitat von xaromz:
Mit dem Zugriff auf die Listenelemente hab ich kein Problem, der ist geschützt.
Zitat von Olli:
Zitat von xaromz:
Danke, ich spreche fließend x86er assembler .
Zitat von Olli:
Zitat von xaromz:
Wie gesagt, der Zugriff auf die Elemente ist gesichert.
Zitat von Olli:
Zitat von xaromz:
Siehe oben.
Zitat von Olli:
Dann stellt sich mir aber die Frage, warum du die Liste immer neu befüllst anstatt den jeweiligen Eintrag zu ergänzen (oder zu ersetzen). Eine doppelt verlinkte Liste ermöglicht dir das einfache Einfügen oder Entfernen von Listenelementen ohne gleich die komplette Liste neu befüllen zu müssen ...
Zitat von Olli:
Zitat von xaromz:
Wenn ich das Programm beende, während der Thread noch läuft, erhält dieser keine Chance, sich sauber zu beenden. Deshalb möchte ich eben beim Beenden des Programms den Hauptthread kurz schlafen legen, damit der Arbeitsthread auf ein Flag reagieren und sich beenden kann.
Zitat von Olli:
Meines Erachtens ist deine Denk- und Herangehensweise zu naiv. Du könntest dir eine Menge Arbeit sparen, wenn jede Liste ein Ausschlußobjekt hat (könnte ein Byte- oder DWORD-Wert sein), der immer nur über die Interlocked-Funktionen bearbeitet wird. Dieses Ausschlußobjekt ist kein echtes Objekt sondern ein Referenzzähler. Da jedes Objekt nun nur noch ein Pointer auf einen Speicherbereich ist, übergibst du dieses an einen Thread nachdem der Aufrufer den Referenzzähler um 1 erhöht hat. Danach kann der Thread erstmal machen was er will, außer daß er davon ausgehen muß, daß die Daten jederzeit von einem anderen Thread gelesen werden können - die Integrität muß also garantiert sein (z.B. kritischer Abschnitt für jede der zu schreibenden Informationen ...). Wenn der (Arbeits-)Thread mit der Abarbeitung fertig ist, legt er sich erstmal selber schlafen (suspend) nachdem er den Referenzzähler um 1 reduziert hat. Dadurch ist er immer an einer wohldefinierten Stelle eingeschlafen. Will der Dispatcher-Thread nun dem Arbeits-Thread sagen, daß er wieder was machen soll, wird der Pointer (globale Variable o.ä.) zum Listeneintrag atomisch ausgetauscht und der Arbeits-Thread wieder aufgeweckt, womit die Schleife im Arbeits-Thread ihre Arbeit weiterführt. Damit ist zwar ein extra Thread (der Dispatcher) nötig, aber die Anzahl der Arbeits-Threads kann ohne Mühe variiert werden. Außerdem mußt du nur einen Listeneintrag in der Listbox aktualisieren, nicht die komplette Liste.
Egal, wie ich den Zugrif steuere (Criticl Section, Interlock-Referenzzähler, Semaphore...), wenn ein Thread läuft, während ich das Programm beende, dann läuft er erstmal weiter und wird dann vermutlich einfach "abgeschossen". Aber ich hab' das gerade so gelöst: - Falls beim Beenden ein Thread läuft, signalisiere ich dem Thread ein Terminate und rufe WaitFor auf - Anschließend werte ich das Flag im Thread aus und setze FreeOnTerminate aus False, damit das Handle in WaitFor erhalten bleibt und keine AV kommt - Sobald WaitFor zurückgekehrt ist, zerstöre ich den Thread mit Free
Zitat von Olli:
Du kannst aber nur Windows sagen, daß dein aktueller Thread seine Rechenzeit abgibt ... an wen, läßt sich der Scheduler von einem Usermode-Programm sicher nicht vorschreiben ... selbst im Kernelmode geht das nur beschränkt.
Aber mein Problem ist ja jetzt gelöst. Gruß xaromz |
Zitat |
Ansicht |
Linear-Darstellung |
Zur Hybrid-Darstellung wechseln |
Zur Baum-Darstellung wechseln |
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 |
LinkBack URL |
About LinkBacks |