Autor Beitrag
LuMa86
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 76



BeitragVerfasst: 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.

ausblenden Delphi-Quelltext
1:
2:
3:
4:
{Nur zur veranschaulichung, im Programm werden die neuen Daten,
welche Thread A ermittelt hat, an das Array in Thread B angehängt.}


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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 600
Erhaltene Danke: 23


Delphi 7 PE
BeitragVerfasst: Do 29.08.13 23:28 
Sonst Bau in A doch ne Queque auf, die du in thread b abarbeitest..

_________________
Sucht "neueres" Delphi :D
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19314
Erhaltene Danke: 1747

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 174
Erhaltene Danke: 43



BeitragVerfasst: Fr 30.08.13 09:53 
user profile iconLuMa86 hat folgendes geschrieben Zum zitierten Posting springen:
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.
ausblenden 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
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 600
Erhaltene Danke: 23


Delphi 7 PE
BeitragVerfasst: 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 ! )
ausblenden volle Höhe 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:
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:
{typen}
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;
{proceduren}
function TContentQueue.IsEmpty: Boolean;
begin
Result := (Head=nilor not Assigned( Head );
end;

procedure TContentQueue.add( aContent: TContent );
begin
if IsEmpty then
  begin
  Head := TContentQueuePart.Create;
  Head.Content := aContent;
  Tail := Head;
  //Tail.Next := nil;  // not so important
  end
else
  begin
  Tail.Next := TContentQueuePart.Create;
  Tail := Tail.next;
  Tail.Content := aContent;
  //Tail.Next := nil;  // not so important
  end;
end;


function TContentQueue.Take: TContent;
begin
if IsEmpty then
  begin
  Result := nil;
  end
else
  begin
  Result := Head.Content;
  // Speicher vom Pointer freigeben und ehemaligen Head freigeben
  // OHNE Content zu löschen ?
  Head := Head.next;
  end;
end;


Nachtrag:

user profile iconOlafSt hat folgendes geschrieben Zum zitierten Posting springen:

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 :D
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!

Für diesen Beitrag haben gedankt: LuMa86
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19314
Erhaltene Danke: 1747

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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:

ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
function TMyThreadedQueue.Count : integer;
begin
  Lock; //Sperrt den Zugriff auf die Queue
  Result:=Count; //Anzahl Einträge in der Queue ermitteln
  UnLock; //Entsperrt die Queue - und der Ärger beginnt
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:
ausblenden 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; //Routinemäßig eingefügt
  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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19314
Erhaltene Danke: 1747

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: Fr 30.08.13 21:19 
user profile iconOlafSt hat folgendes geschrieben Zum zitierten Posting springen:
So grob skizziert ;) Mir ist klar, das nun etliche Aufschreien werden "OH NOEZ GLOBAL VARS ARE EVIL" - aber zeigt mir ne besser Möglichkeit...
Wie ich schon geschrieben habe... TMonitor...
ausblenden 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:
24:
25:
26:
27:
28:
29:
// im Konstruktor oder so:
TMonitor.SetSpinCount(MyQueue, 500);

procedure TWorkThread.Execute;
var
  Data: TMyObject;
begin
  while not Terminated do
  begin
    Data := nil;
    TMonitor.Enter(MyQueue);
    try
      if MyQueue.Count > 0 then
        Data := MyQueue.Pop;
    finally
      TMonitor.Exit(MyQueue);
    end;
    if Assigned(Data) then
    begin
      try
        ...
      finally
        Data.Free;
      end;
    end
    else
      Sleep(500);
  end;
end;
// EDIT:
Außerdem könntest du eine Klasse von TQueue ableiten und die Critical Section dort unterbringen.


Zuletzt bearbeitet von jaenicke am Sa 31.08.13 06:59, insgesamt 1-mal bearbeitet

Für diesen Beitrag haben gedankt: LuMa86
LuMa86 Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starontopic star
Beiträge: 76



BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic starofftopic star
Beiträge: 486
Erhaltene Danke: 99

Win7, Win81, Win10
Tokyo, VS2017
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19314
Erhaltene Danke: 1747

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 600
Erhaltene Danke: 23


Delphi 7 PE
BeitragVerfasst: 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 :D
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
jaenicke
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 19314
Erhaltene Danke: 1747

W11 x64 (Chrome, Edge)
Delphi 11 Pro, Oxygene, C# (VS 2022), JS/HTML, Java (NB), PHP, Lazarus
BeitragVerfasst: So 01.09.13 13:20 
Den Pointer musst du nicht freigeben, nein, nur irgendwann das Objekt.
IhopeonlyReader
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 600
Erhaltene Danke: 23


Delphi 7 PE
BeitragVerfasst: 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?

ausblenden 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 :D
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
Blup
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 174
Erhaltene Danke: 43



BeitragVerfasst: 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
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 600
Erhaltene Danke: 23


Delphi 7 PE
BeitragVerfasst: 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 :D
Wer nicht brauch was er hat, brauch auch nicht was er nicht hat!
Blup
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Beiträge: 174
Erhaltene Danke: 43



BeitragVerfasst: 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:
ausblenden 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
  {hier eigene Sachen aufräumen/freigeben}

  {Sachen die im Vorfahren deklariert sind aufräumen}
  inherited;

  {eventuell auch hier eigene Sachen aufräumen/freigeben die nicht vom Vorfahren abhängig sind}
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:
ausblenden 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.