![]() |
CriticalSection und Threads
Aus aktuellem Anlass und weil mit das Handling der CriticalSections gerade beim Vererben von Threads etwas genervt hat hier mal ein smarter Ansatz:
Wenn in einem Thread eine CriticalSection benötigt wird (Schutz der Datenzugriffe durch unterschiedliche Threads), dann sollte die CriticalSection folgendermassen erzeugt werden:
Delphi-Quellcode:
Die CriticalSection wird noch vor der eigentlichen Objekt-Instanz erzeugt und schützt auch schon das Erzeugen dieser Instanz.
unit uThreadA;
interface uses Classes, SyncObjs; type TMyThreadA = class( TThread ) strict private _CS : TCriticalSection; protected function CS : TCriticalSection; public constructor Create( CreateSuspended : Boolean ); destructor Destroy; override; end; implementation { TMyThreadA } function TMyThreadA.CS : TCriticalSection; begin if not Assigned( _CS ) then _CS := TCriticalSection.Create; Result := _CS; end; constructor TMyThreadA.Create( CreateSuspended : Boolean ); begin CS.Enter; try inherited; finally CS.Leave; end; end; destructor TMyThreadA.Destroy; begin CS.Enter; try inherited; finally CS.Leave; // Eigentlich FreeAndNil( _CS ) aber wenn man sich die SysUtils sparen kann ;o) _CS.Free; _CS := nil; end; end; end. Die CriticalSection wird nach der Freigabe der Objekt-Instanz freigegeben und die Freigabe der Objekt-Instanz wird von der CriticalSection noch geschützt. So weit, so gut ... Aber was passiert, wenn man diese Thread-Klasse vererben möchte?
Delphi-Quellcode:
Eine Alternative wäre, in jedem Thread (auch den abgeleiteten) eine neue CS einzuführen.
unit uThreadB;
interface uses Classes, SyncObjs, uThreadA; type TMyThreadB = class( TMyThreadA ) public constructor Create( CreateSuspended : Boolean ); destructor Destroy; override; end; implementation { TMyThreadB } constructor TMyThreadB.Create( CreateSuspended : Boolean ); begin CS.Enter; try inherited; finally CS.Leave; end; end; destructor TMyThreadB.Destroy; begin CS.Enter; try // inherited // darf hier nicht stehen, denn sonst wird ja die CS freigegeben // und im finally-Teil gibt es eine Exception finally CS.Leave; end; inherited; end; end. Warum kann die CS denn kein Interface sein? ;o) Dann könnte man das sehr hübsch so machen:
Delphi-Quellcode:
und der davon abgeleitete Thread wird auf die gleiche Weise implementiert, denn nun verflüchtigt sich die CriticalSection dann, wenn diese nicht mehr benötigt wird.
unit uThreadA;
interface uses Classes, SyncObjs; type TMyThreadA = class( TCSThread ) // eine neue Basisklasse Thread mit CS public constructor Create( CreateSuspended : Boolean ); destructor Destroy; override; end; implementation { TMyThreadA } constructor TMyThreadA.Create( CreateSuspended : Boolean ); begin CS.Enter; try inherited; finally CS.Leave; end; end; destructor TMyThreadA.Destroy; begin CS.Enter; try inherited; finally CS.Leave; end; end; end.
Delphi-Quellcode:
Und hier das Interface mit der Thread-Klasse:
unit uThreadB;
interface uses Classes, SyncObjs, uThreadA; type TMyThreadB = class( TMyThreadA ) public constructor Create( CreateSuspended : Boolean ); destructor Destroy; override; end; implementation { TMyThreadB } constructor TMyThreadB.Create( CreateSuspended : Boolean ); begin CS.Enter; try inherited; finally CS.Leave; end; end; destructor TMyThreadB.Destroy; begin CS.Enter; try inherited finally CS.Leave; end; end; end.
Delphi-Quellcode:
unit uCSObjects;
interface uses Classes, SyncObjs; type ICriticalSection = interface ['{DE0BF9E0-92C0-424B-A70F-5C58CD412C1A}'] procedure Enter; function TryEnter : Boolean; procedure Leave; end; TInterfacedCriticalSection = class( TInterfacedObject, ICriticalSection ) strict private _CS : TCriticalSection; protected procedure Enter; function TryEnter : Boolean; procedure Leave; public constructor Create; destructor Destroy; override; end; TCSThread = class( TThread ) strict private _CS : ICriticalSection; protected function CS : ICriticalSection; end; implementation { TInterfacedCriticalSection } constructor TInterfacedCriticalSection.Create; begin inherited; _CS := TCriticalSection.Create; end; destructor TInterfacedCriticalSection.Destroy; begin _CS.Free; _CS := nil; inherited; end; procedure TInterfacedCriticalSection.Enter; begin _CS.Enter; end; procedure TInterfacedCriticalSection.Leave; begin _CS.Leave; end; function TInterfacedCriticalSection.TryEnter : Boolean; begin Result := _CS.TryEnter; end; { TCSThread } function TCSThread.CS : ICriticalSection; begin if not Assigned( _CS ) then _CS := TInterfacedCriticalSection.Create; Result := _CS; end; end. |
AW: CriticalSection und Threads
Zitat:
> es können mehrere TCriticalSection erstellt werden = Speicherleck = die direkten nachfolgenden Aufrufe wären nicht geschützt, da sie CriticalSections nutzen würden. = es würde zu einem Deadlock kommen *1 1) CS.Enter würde eventuell, beim Erstellen, unterschiedliche CriticalSections, aber CS.Leave würde richtig funktionieren und es würden dann einige Leaves auf eine falsche CS zeigen, was die Sperr-Zählung schrottet. Lösung: Erstell die CS schon im Constructor, denn der ist immer threadsave, Dank des Highlanderprinzips. Wenn sie unbeding erst in dieser Funktion erstellt werden soll, dann sag Bescheid und gib mir ein Minütchen. |
AW: CriticalSection und Threads
Der Einwand ist berechtigt, jedoch das Problem ist eher theoretisch, denn eine CS für eine Instanz sollte so implementiert werden:
Delphi-Quellcode:
Ein Durcheinander kann somit nur dann auftreten, wenn ich es schaffe von 2 unterschiedlichen Threads die gleiche Objekt-Instanz zu erzeugen.
constructor TMyThreadA.Create( CreateSuspended : Boolean );
begin CS.Enter; // erster Aufruf für die neue Instanz try inherited; // ab jetzt gibt es eine Instanz finally CS.Leave; end; end; Und das ist meines Wissens nicht möglich. Um auch diesen theoretischen Fall abzusichern, könnte man noch eine CS für die Unit verwenden, die dieses dann regeln würde.
Delphi-Quellcode:
unit uCSObjects;
interface [...] implementation var __CS : ICriticalSection; // Eine CriticalSection für diese Unit { TInterfacedCriticalSection } [...] { TCSThread } function TCSThread.CS : ICriticalSection; begin __CS.Enter; try if not Assigned( _CS ) then _CS := TInterfacedCriticalSection.Create; finally __CS.Leave; end; Result := _CS; end; initialization __CS := TInterfacedCriticalSection.Create; end. |
AW: CriticalSection und Threads
Warum haben deine Konstruktoren denn überhaupt Seiteneffekte? Denn nur dann braucht man ja die CriticalSections.
mfg Christian |
AW: CriticalSection und Threads
Ich verstehe das Problem nicht. Ich erzeuge meine privaten Felder, also auch eine CS, im Konstruktor und gebe sie im Destruktor wieder frei.
Weiterhin vestehe ich nicht, weshalb ich sowohl Konstruktor als auch Destruktor gegen parallele Aufrufe schützen soll. Versucht man hier, ein Problem zu lösen, das es bei halbwegs brauchbarer Programmierung gar nicht geben kann? Konstruktoren sind singulär, aber Destruktoren könnten gleichzeitig aufgerufen werden. Aber nur, wenn ich schlampig programmiere. Bei mir gibt es klare Zuständigkeiten. Wer ein Objekt instantiiert, gibt es auch wieder frei. Baste. Natürlich schütze ich Daten gegen simultane Zugriffe, aber speziell Threads instantiiere ich an einer Stelle und gebe sie dort auch wieder frei. Singulär und wohldefiniert. Threads gebe ich dann frei, wenn alle Verwender des Threads entweder selber alle tot sind oder Bescheid wissen, das der freizugebende Thread seine Arbeit beendet hat. Die einzigen Objekte, die keine direkte Zuständigkeit haben, sind Singletons. Diese werden -wupps- irgendwann (z.B. on demand) erzeugt und schweben frei im Raum. Hier muss man wirklich etwas aufpassen. Allerdings existiert hier auch nur ein einziger Destroy-Aufruf. Zu doppelten Aufrufen kann es dann gar nicht kommen, höchstens zu Verwendung, nachdem das Singleton freigegeben wurde. |
AW: CriticalSection und Threads
Zitat:
Aber wenn ich Thread-Safe Objekte benötige, dann könnten auch die Konstruktoren Seiteneffekte haben, je nachdem was ich im Konstruktor mache. z.B. könnte ich dort die Instanz an einen bestehen Thread übergeben Wäre der Konstruktor also nicht geschützt, dann würde es sofort knallen, wenn ich mit dem Konstruktor noch nicht fertig bin und der Thread schon auf meine Instanz zugreift. Ich stelle einfach damit sicher, das es nicht knallen kann. Es geht also darum, wie ich Objekte Thread-Safe bekomme - und ich möchte meine Objekte generell Thread-Safe haben und nicht nur unter ganz bestimmten Bedingungen. Warum sollte man eine Klasse so deklarieren?
Delphi-Quellcode:
Ganz einfach, im Moment beinhaltet der Konstruktor von TObject keinen Code, aber er könnte irgendwann mal Code haben und ich bin auch in Zukunft safe.
TFoo = class // Ableitung von TObject
public constructor Create; end; constructor TFoo.Create; begin inherited; // wofür sollte das gut sein, denn TObject.Create beinhaltet nichts // irgendwas end; |
AW: CriticalSection und Threads
Delphi-Quellcode:
Braucht keine zusätzliche CS.
function TMyThreadA.CS: TCriticalSection;
var Comp: TCriticalSection; begin Result := _CS; if not Assigned(Result) then begin Result := TCriticalSection.Create; Comp := InterlockedCompareExchangePointer(Pointer(_CS), Pointer(Result), nil); if Assigned(Comp) then begin Result.Free; Result := Comp; end; end; end; Nur im Destructor, vor dem Freigeben der CS, diese dort besser nochmal mal sperren.
Delphi-Quellcode:
Nicht daß sie noch in Benutzung ist, beim Freigeben.
CS.Enter;
CS.Free; |
AW: CriticalSection und Threads
Zitat:
Zudem sind es ja normalerweise nicht die Threads, die Thread-Safe sein müssen, sondern die Objekte, auf die die Threads zugreifen. Zitat:
Zitat:
Zitat:
Zitat:
mfg Christian |
AW: CriticalSection und Threads
Zitat:
gerade das kann ich damit ja verhindern ... Zitat:
Benutze ich hier aber beispielhaft um das zu erläutern, denn Thread-Safe benötige ich ja nur im Zusammenhang mit Threads. Aber welches Objekt man auf die skizzierte Art und Weise schützt ist egal. |
AW: CriticalSection und Threads
Zitat:
Was du hier also beschriebst, ist eine Anwendung des ![]() Trotzdem fällt mir kein sinnvoller Fall ein, in dem das explizite Schützen des Konstruktors helfen sollte. Denn wie du selbst sagst, dazu müssten zwei Threads gleichzeitig das selbe (und nicht nur das gleiche) Objekt erzeugen, was ausgeschlossen ist. Denn, selbst wenn du Seiteneffekte im Konstruktor hast, müsste das Ziel der Seiteneffekte Thread-Safe sein und nicht der Konstruktor selbst. Ansonsten hindert dich ja nichts daran das genannte "Ziel" gleichzeitig aus nem anderen Thread zu bearbeiten. Daneben hilft dein Ansatz auch nicht, wenn du gleichzeitig zwei verschiedene Instanzen der selben Klasse erzeugst, wenn im Konstruktor eine nicht thread-safer Seiteneffekt ausgeführt wird, da es sich m zwei unterschiedliche CriticalSections handelt. mfg Christian |
AW: CriticalSection und Threads
Kann mir einer mal ein Beispiel nennen, wo ein Konstruktor knallen kann, weil 'er noch nicht fertig ist'?
Wenn ich schreibe:
Delphi-Quellcode:
Hat Foo erst dann einen Wert, wenn der Konstruktor fertig ist.
Foo := TMyThread.Create;
Da es eine Grundregel gibt, die besagt, das Konstruktoren keinen logischen Code beinhalten sollen, kann ich mir irgendwelche komplex konstruierten Gebilde sparen, wo ich vielleicht doch auf ein Objekt zugreife, dessen Konstruktor noch nicht abgearbeitet ist, z.B.:
Delphi-Quellcode:
So, dagegen kann man sich also absichern, ja? Wozu? Sowas macht man einfach nicht:
Constructor TMyThread.Create (aSomeObject : TSomeObject);
Begin ... aSomeObject.CallMe(Self); ... End; Procedure TSomeObject.CallMe (aThread : TMyThread); Begin aThread.Bang(); End;
Delphi-Quellcode:
Der Verwender des TMyThread muss eben Initialize() aufrufen. Und das kann er erst, wenn die Instanz (vollständig) instantiiert wurde.
Constructor TMyThread.Create (aSomeObject : TSomeObject);
Begin ... FSomeObject := aSomeObject; ... End; Procedure TMyThread.Initialize(); Begin FSomeObject.CallMe(Self); End; Procedure TSomeObject.CallMe (aThread : TMyThread); Begin aThread.Bang(); End; Ich bleib dabei: Diese Gedankenspiele sind nett, aber lenken vom eigentlichen Problem ab. Sofern man das Gedankenspiel nicht als solches auffässt. Wie wäre es mit Überlegungen, die Endlosschleifen erkennen?
Delphi-Quellcode:
Obwohl ich ein System kenne, das versucht, soetwas zu erkennen (so ein Schwachsinn).
While True do;
|
AW: CriticalSection und Threads
Zitat:
Allerdings: Der oben beschriebene Ansatz löst dieses Problem, wie ich oben schon geschrieben habe, gerade *nicht*. Und ein paar andere Fälle löst er auch nicht. Im Grunde genommen gebe ich dir also damit recht, dass die CS im Konstruktor nichts bringt. mfg Christian |
AW: CriticalSection und Threads
Zitat:
![]() Jedes Objekt "enthält" ab Delphi 2009 ein "Lock", das wie eine Critical Section benutzt werden kann (und noch mehr kann wie z.B. PulseAll und Wait) - und es verfällt automatisch mit Ablauf der Objekt-Lebenszeit. Dass Threads von außen nach ihrem Start aufgerufen werden erscheint mir ansonsten auch etwas ungewöhnlich - in welchem Status der Thread gerade ist, wenn man eine seiner Operationen aufruft, ist unbestimmt. |
AW: CriticalSection und Threads
Zitat:
Der Schutz des Konstruktors wird dann wichtig, wenn man z.B. eine Klasse erzeugt, die z.B. einen Thread-Pool erstellt und diesem Thread-Pool auch sich selbst als Referenz mitgibt. Hiermal als völlig ungeschützte Klasse:
Delphi-Quellcode:
Mit meiner Lösung bekomme ich die Klasse 100% Thread-Safe mit relativ geringem Aufwand.
TMyThreadPool = class
private _Pool : TList<TPoolThread>; public constructor Create( PoolSize : integer ); // Some Properties ... end; constructor TMyThreadPool.Create; begin inherited Create; _Pool := TObjectList<TPoolThread>.Create; while _Pool.Count < PoolSize do _Pool.Add( TPoolThread.Create( Self ) ); // Init Some Properties end; Eine Ableitung von der Klasse TMyThreadPool geht auch sehr charmant und bleibt 100% Thread-Safe |
AW: CriticalSection und Threads
Man könnte hier natürlich darüber streiten, ob die Pool-Threads den Pool überhaupt kennen sollten, aber hier hast du recht. Immer, wenn man im Konstruktor einen Thread erzeugt und dem Self mitgibt hat man das Problem und deine Lösung hilft hier. OK, verstanden.
mfg Christian |
AW: CriticalSection und Threads
Ich bleibe dabei: Trenne Instantiierung von Initialisierung und man hat viel mehr Zeit für andere Sachen. Weil man sich nicht mit solchem "Quark" beschäftigen muss. Wobei das Problem an sich schon recht interessant ist.
|
AW: CriticalSection und Threads
Zitat:
mfg Christian |
Alle Zeitangaben in WEZ +1. Es ist jetzt 18:03 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