Autor |
Beitrag |
LuMa86
      
Beiträge: 76
|
Verfasst: Do 29.08.13 22:29
Hallo,
ich arbeite mittlerweile schon eine Weile mit Threads, allerdings bin ich doch noch ein Anfänger auf dem Gebiet. Um meine Anwendung "Thread-Safe" zu gestalten sind mir 2 Mittel bekannt: Synchronisieren und TCriticalSection. Ich möchte folgendes realisieren:
In meiner Anwendung laufen 2 Threads, Thread A und Thread B. Thread A ermittelt die Prüfsumme von Dateien (SHA2, bzw. SHA512) aus einem Arbeitsverzeichnis (ca. 200-500GB). Nachdem er 1GB an Daten gehasht hat, übergibt er ein Array, mit verschiedenen Informationen, u.a. auch dem Hash an Thread 2.
Delphi-Quelltext 1: 2: 3: 4:
|
ThreadB.FileInfo := ThreadA.FileInfo; |
Bevor ich jetzt mein Problem erläutere noch einen kurzen Programmablauf:
- Thread A sammelt die Dateiinfo's, Thread B dreht solange Däumchen
- Thread A hat 1GB Daten gechacht und übergibt diese nun an Thread B (Siehe Codeschnipsel oben)
- Thread B führt nach der Übergabe des Arrays aufwendige Berechnungen durch, währen Thread A wieder Dateiinfo's sammelt
- Thread A hat wieder 1GB gesammelt und übergibt die Daten an Thread B, dieser ist aber noch mit der Berechnung der vorherigen Daten
beschäftigt --> Problem
Hier tritt das Problem auf. Wenn Thread A die Daten übergibt und Thread B noch nicht fertig ist, kommt es zu Problemen, da das Array unvollständig sein könnte, oder es zu anderen Problemen kommen kann (Schleifenlänge verändert sich auf einmal?).
So wie ich das verstanden habe, schützt TCriticalSection nur gegen mehrfachen Schreibzugriff, aber lesen ist ja jederzeit möglich. Vllt. hab ich auch was übersehen und nicht richtig durchdacht, aber ich weiß einfach nicht weiter.
Hat jemand eventuell eine Idee, wie ich das löse? Oder besser mal eine Frage: Wie schaffe ich es, Thread A erst die Daten übergeben zu lassen, wenn Thread B wieder im "Standby" ist? Thread A müsste ja nicht rumlungern, sondern einfach wieder ein paar Dateien checken, bis Threab B fertig ist. Ob es jetzt 1GB oder 1,5GB sind, die gecacht wurden, macht ja keinen großen Unterschied.
Ich bin für jede Hilfe dankbar
Gruß,
LuMa
|
|
IhopeonlyReader
      
Beiträge: 600
Erhaltene Danke: 23
Delphi 7 PE
|
Verfasst: Do 29.08.13 23:28
Sonst Bau in A doch ne Queque auf, die du in thread b abarbeitest..
_________________ Sucht "neueres" Delphi
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
|
|
jaenicke
      
Beiträge: 19314
Erhaltene Danke: 1747
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Fr 30.08.13 05:44
Entweder das oder du nimmst z.B. Pipes. Dann kann der eine Thread einfach hineinschreiben und der andere liest aus sobald er Daten braucht. Sollte der zweite Thread nicht hinterher kommen, könntest du dann auch sehr einfach einen zweiten nehmen, der auch aus der selben Pipe liest.
Die Synchronisation übernimmt Windows dann, deshalb wäre es für dich relativ einfach.
Für diesen Beitrag haben gedankt: LuMa86
|
|
Blup
      
Beiträge: 174
Erhaltene Danke: 43
|
Verfasst: Fr 30.08.13 09:53
LuMa86 hat folgendes geschrieben : | So wie ich das verstanden habe, schützt TCriticalSection nur gegen mehrfachen Schreibzugriff, aber lesen ist ja jederzeit möglich. Vllt. hab ich auch was übersehen und nicht richtig durchdacht, aber ich weiß einfach nicht weiter. |
TCriticalSection schützt bestimmte Codeabschnitte vor der gleichzeitigen Ausführung aus unterschiedlichen Threads.
Für ein Property wird dazu im Getter und Setter die selbe TCriticalSection benutzt. So kann wärend des Schreibens nicht gelesen werden und wärend des Lesens nicht geschrieben. Gleichzeitiges Lesen oder gleichzeitiges Schreiben geht auch nicht.
TMultiReadExclusiveWriteSynchronizer ermöglicht zumindest das gleichzeitige Lesen.
ThreadB könnte eine Liste von FileInfo halten in die ThreadA neue Objekte FileInfo ablegt.
ThreadB kann diese dann abholen. Jeder Zugriff auf die Liste muss natürlich über eine TCriticalSection geschützt werden.
Die grundsätzlich bessere Idee wäre, die Aufgaben nicht in den Threads sondern als eigenständige Klassen zu implementieren. Die Aufgaben kommen in eine Liste und jeder freie Thread holt sich eine neue Aufgabe ab und führt diese aus. So kann man die Anzahl der Threads an die Hardware anpassen und beliebige Aufgaben abarbeiten lassen.
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
| procedure TMyThread.Execute; begin while not Terminated and HoleNeueAufgabe do begin FAufgabe.Execute; end; end;
procedure TDateiAufgabe.Execute; begin Lies1GBDaten; ErzeugeNeueDateiAufgabeWennWeitereDateien; UmfangreicheBerechnungen; ErgebnisAbliefern; end; |
Für diesen Beitrag haben gedankt: LuMa86, turbo
|
|
OlafSt
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: Fr 30.08.13 11:28
Die Zwei-Thread-Lösung ist hier doch genauso passend. Ein Thread rechnet die Checksummen, der andere regelt den Rest.
Ich persönlich benutze bei Multithreaded-Problemen, wo ein Thread auf die Ergebnisse des anderen wartet, sehr oft Queues. So kann der Checksummen-Thread in aller Ruhe seinen Job machen, während Thread 2 einfach darauf wartet, das in die Queue irgendwas eingeschoben wird. Ist das der Fall, nimmt T2 den Kram heraus und macht seine Arbeit.
So können beide Threads nebeneinander laufen ohne aufwändige Synchronisations- und Sicherungsmechanismen. Einzig von TQueue sollte man eine neue Klasse ableiten, die dann Zugriffe via Push/Pop mit einer TCriticalsection absichert (kA ob die XE-Versionen inzwischen solche Queues im Programm haben).
_________________ Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Für diesen Beitrag haben gedankt: LuMa86
|
|
IhopeonlyReader
      
Beiträge: 600
Erhaltene Danke: 23
Delphi 7 PE
|
Verfasst: Fr 30.08.13 16:48
zu den Queques...
ich nenn deine 1GB zu übergebenden Daten einfach mal TContent (darin stehen dann halt deine ganzen Daten (wobei TContent eine Klasse ist ! )
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54:
| type TContentQueuePart = class public Content: TContent; Next: TContentQueuePart; end; TContentQueue = class private Head, Tail: TContentQueuePart; public function IsEmpty: Boolean; procedure add( aContent: TContent ); function Take: TContent; end;
function TContentQueue.IsEmpty: Boolean; begin Result := (Head=nil) or not Assigned( Head ); end;
procedure TContentQueue.add( aContent: TContent ); begin if IsEmpty then begin Head := TContentQueuePart.Create; Head.Content := aContent; Tail := Head; end else begin Tail.Next := TContentQueuePart.Create; Tail := Tail.next; Tail.Content := aContent; end; end;
function TContentQueue.Take: TContent; begin if IsEmpty then begin Result := nil; end else begin Result := Head.Content; Head := Head.next; end; end; |
Nachtrag:
OlafSt hat folgendes geschrieben : |
Einzig von TQueue sollte man eine neue Klasse ableiten, die dann Zugriffe via Push/Pop mit einer TCriticalsection absichert (kA ob die XE-Versionen inzwischen solche Queues im Programm haben). |
Seit wann gibt es eine Klasse TQueue? Ich habe sie mir immer selbst gebaut und entsprechend angepasst :O
_________________ Sucht "neueres" Delphi
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
Für diesen Beitrag haben gedankt: LuMa86
|
|
jaenicke
      
Beiträge: 19314
Erhaltene Danke: 1747
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Fr 30.08.13 20:08
Bei deiner Queue fehlt die Threadsicherheit, z.B. mit einer Critical Section oder TMonitor (bei letzterem nicht vergessen den Spin Count zu setzen aus Geschwindigkeitsgründen).
TQueue gibt es seit mindestens Delphi 7.
Eine threadsichere Queue gibt es in Form von TThreadedQueue<T> generisch zumindest seit XE4.
Für diesen Beitrag haben gedankt: LuMa86
|
|
OlafSt
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: Fr 30.08.13 20:21
Ich habe spaßenshalber mal ein wenig quergelesen. Meine Idee mit der selbstgebauten threadsicheren TQueue ist womöglich gar keine so gute - denn es gibt immer wieder Probleme mit der Abfrage von Werten oder Zuständen. Zum Beispiel:
Delphi-Quelltext 1: 2: 3: 4: 5: 6:
| function TMyThreadedQueue.Count : integer; begin Lock; Result:=Count; UnLock; end; |
Das Problem ist, das unser neues Count die Queue sperrt, den Wert ermittelt und die Queue freigibt. Bevor der Rückgabewert aber an den Aufrufer gelangt, kann ein Threadwechsel stattfinden und dann aus eben derselben Queue ein Element herausgezogen werden. Beim nächsten Threadwechsel entspricht der Rückgabewert von TMyThreadedQueue.Count schon nicht mehr dem aktuellen Stand.
Ich habs eigentlich aus Bequemlichkeit getan (dachte ich bisher) - aber ich sperre Zugriffe auf solche Queues stets mit einer globalen TCriticalSection. Der Aufrufer sperrt dann die Queue und erst, wenn sein Job beendet ist (wie z.B. Count abfragen und irgendwas damit veranstalten) wird die Queue wieder freigegeben:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:
| ... var globalCriticalSection: TCriticalSection; ... procedure TWorkThread.Execute; var i: integer; Data: TMyObject; begin while not Terminated do begin globalCriticalSection.Enter; if MyQueue.Count > 0 then begin Data:=MyQueue.Pop; GlobalCriticalSection.Leave; ... Data.Free; end else GlobalCriticalSection.Leave; Sleep(500); end; |
So grob skizziert  Mir ist klar, das nun etliche Aufschreien werden "OH NOEZ GLOBAL VARS ARE EVIL" - aber zeigt mir ne besser Möglichkeit...
_________________ Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
Für diesen Beitrag haben gedankt: LuMa86
|
|
jaenicke
      
Beiträge: 19314
Erhaltene Danke: 1747
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Fr 30.08.13 21:19
Zuletzt bearbeitet von jaenicke am Sa 31.08.13 06:59, insgesamt 1-mal bearbeitet
Für diesen Beitrag haben gedankt: LuMa86
|
|
LuMa86 
      
Beiträge: 76
|
Verfasst: Fr 30.08.13 23:22
Da schaut man mal kurz nicht und ihr füllt mir den ganzen Thread. Danke erstmal das ihr mich nochmal über TCriticalSection aufgelärt habt  Also ich denke ich bastel mir meine eigene Klasse auf Basis von TQueue zusammen. Da in Zukunft noch bisschen was dazukommt (an Datenübergabe zwischen Threads), behalte ich so auch den Überblick. Außerdem habt ihr mir ja einiges an Code hinterlassen, da kann ja nichts schiefgehen
Danke an alle, die sich beteiligt haben 
|
|
OlafSt
      
Beiträge: 486
Erhaltene Danke: 99
Win7, Win81, Win10
Tokyo, VS2017
|
Verfasst: Sa 31.08.13 12:28
Von TMonitor habe ich jetzt das erste mal was gehört  Ein wenig herumgooglen zeigt, das diese Klasse doch noch das eine oder andere Problem hat, das wohl erst mit XE3 Upd4 endgültig gelöst wurde. Für mich als D2010-Nutzer fällt das also erstmal flach, bis alle Projekte auf XE4 umgestellt sind.
_________________ Lies, was da steht. Denk dann drüber nach. Dann erst fragen.
|
|
jaenicke
      
Beiträge: 19314
Erhaltene Danke: 1747
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: Sa 31.08.13 12:59
Das größte Problem gibt es mit TMonitor, wenn man nicht sauber alle Objekte freigibt. Gibt man nämlich ein Objekt nicht frei, das man mit TMonitor benutzt hat, hängt sich die Anwendung beim Beenden auf und muss abgeschossen werden.
Bei sauberer Nutzung hatte ich damit auch bei XE keine Probleme.
|
|
IhopeonlyReader
      
Beiträge: 600
Erhaltene Danke: 23
Delphi 7 PE
|
Verfasst: So 01.09.13 12:39
Wo wir nebenbei auch bei Queques sind, wie ist es möglich bei "meiner" procedure Take
den "alten" Head freizugeben ohne das Okjekt (Content) freizugeben? Den Pointer next brauch man eigentlich nicht freigeben oder?
Integervariablen etc, kann man ja auch nicht freigeben..
_________________ Sucht "neueres" Delphi
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
|
|
jaenicke
      
Beiträge: 19314
Erhaltene Danke: 1747
W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
|
Verfasst: So 01.09.13 13:20
Den Pointer musst du nicht freigeben, nein, nur irgendwann das Objekt.
|
|
IhopeonlyReader
      
Beiträge: 600
Erhaltene Danke: 23
Delphi 7 PE
|
Verfasst: So 01.09.13 13:53
Bei meiner Queue habe ich ja ein
TContentQueuePart
und ein
TContent
ich müsste dann doch sowohl TContentQueuePart freigeben, wenn ich Take benutze (ohne TContent freizugeben) und
TContent wird ja vom Aufrufer vom Take freigegeben
oder muss TContentQueuePart nicht freigegeben werden, da dort "nur" ein pointer beinhaltet ist?
aber das hieße ja, ich müsste eine "leere" klasse nie freigeben, oder?
Delphi-Quelltext 1: 2: 3: 4: 5:
| type leereKlasse = class private public end; |
müsste man das freigeben, wenn man es erzeugt hat? nach deiner aussage ja nicht
_________________ Sucht "neueres" Delphi
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
|
|
Blup
      
Beiträge: 174
Erhaltene Danke: 43
|
Verfasst: Mi 04.09.13 09:21
Wird "leereKlasse.Create" aufgerufen, muss auch Free aufgerufen werden.
Auch wenn bei der Deklaration nicht angegeben, ist die Klasse zumindest von TObject abgeleitet.
Durch den Konstruktor wird indirekt ein Speicherbereich reserviert, der zumindest einen Zeiger auf die Klasse enthält.
Damit arbeiten Funktionen wie z.B. ClassType, InheritsFrom.
Hat die Klasse weitere Member-Variablen, ist dieser Speicherbereich entsprechend größer.
Der Rückgabewert des Konstruktors ist ein Zeiger auf eben diesen Speicher.
Alle Methoden der Klasse bekommen diesen Zeiger als versteckten Parameter (Self) übergeben.
Der Destruktor räumt diesen Speicher auf und gibt ihn zum Schluss indirekt frei.
|
|
IhopeonlyReader
      
Beiträge: 600
Erhaltene Danke: 23
Delphi 7 PE
|
Verfasst: Mi 04.09.13 10:38
was mit den beinhalteten Objekten in der Klasse`?
werden diese mit freigebeben wenn die klasse "befreit" wird? oder werden nur die pointer/integer variablen etc. "geleert", also wenn ich die Speicherdresse des beinhalteten Objekts noch wo anders habe, kann ich weiterhin darauf zugreifen
_________________ Sucht "neueres" Delphi
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
|
|
Blup
      
Beiträge: 174
Erhaltene Danke: 43
|
Verfasst: Mi 04.09.13 11:35
Erstellt bzw. besitzt die Klasse eigene Unterobjekte, so ist die Klasse dafür verantwortlich diese freizugeben.
Dafür gibt es den Destruktor der Klasse.
Dieser sollte immer in dieser Form deklariert werden, damit Free auch den richtigen Destruktor aufrufen kann:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15:
| type TMyClass = class() destructor Destroy; override; end;
destructor TMyClass.Destroy; begin
inherited;
end; |
Die Basisklasse TComponent implementiert z.B. eine Liste von Objekten.
Diese Liste wird im Destruktor von TComponent freigegeben und gibt alle enthaltenen Objekte frei.
Besitzt die Klasse das Unterobjekt nicht, sondern hält nur ein Property, das auf das Unterobjekt verweist, darf man dieses im Destruktor natürlich nicht freigeben.
Bespiel:
Delphi-Quelltext 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23:
| type TMyObject = class;
TMyWriter = class private FStream: TStream; public procedure WriteToStream(AObject: TMyObject); property Stream: TStream read FStream write FStream; end;
procedure TMyObject.ExportTo(AStream: TStream); var Writer: TMyWriter; begin Writer := TMyWriter.Create; try Writer.Stream := AStream; Writer.WriteToStream(Self); finally Writer.Free; end; end; |
Die Prozedur ist Besitzer der Instanze von TMyWriter und gibt diese frei.
Writer ist aber nicht Besitzer des Streams. Der Stream sollte dort freigegeben werden, wo er auch erzeugt wurde.
|
|
|