![]() |
Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Hallo!
Ich habe in meinem Programm eine globale Instanz eines Threads laufen mit einer TObjectList drin. Bei dem Thread handelt es sich um eine Art Verarbeitungs-Queue, in der Aufgaben in Form von Items abgelegt und vom Thread dann verarbeitet werden. Auf die Thread-Instanz greifen mehrere andere Threads zu und zwar ausschließlich mit dem Ziel, der TObjectList neue Objekte (Aufgaben) hinzuzufügen. Dafür ist eine public-deklarierte Funktion innerhalb des Threads vorgesehen. Reicht es in diesem Fall aus, wenn ich nur die TObjectList mit einer TCriticalSection (beide innerhalb der Objekt-Instanz deklariert) schütze oder sollte ich bereits die Zugriffe auf die eingenliche Thread-Instanz mit einer global deklarierten TCriticalSection synchronisieren? Als ich die Klasse erstellt habe, habe ich mir gedacht, dass auf die eigentliche Thread-Instanz eher nur lesend zugegriffen wird und erst die TObjectList geschützt werden sollte. Ist es überhaupt richtig, die Liste innerhalb eines Threads zu erstellen und die dann mithilfe eier public-Funktion zu beschreiben oder wäre es besser, die Liste und die CriticalSection als globale Objekte zu deklarieren und dem Thread nur die Abarbeitung der Aufgaben anzuvertrauen? Aktuell sieht es so aus bei mir:
Delphi-Quellcode:
type
TapDataReceiverQueueItem = class private FClientData: string; FTarget: TapDataTarget; FPriority: TapPriority; public property ClientData: string read FClientData write FClientData; property Target: TapDataTarget read FTarget write FTarget; property Priority: TapPriority read FPriority write FPriority; end; type TapDataReceiverQueue = class(TThread) constructor Create(CreateSuspended: Boolean); destructor Destroy; override; private FQueue: TObjectList; FcsQueue: TCriticalSection; protected procedure Execute; override; procedure DeleteFromQueue(AIndex: integer); public procedure AddToQueue(AClientData: string; ATarget: TapDataTarget; APriority: TapPriority); function Count: integer; end; var DataReceiverQueue: TapDataReceiverQueueItem; implementation ... constructor TapDataReceiverQueue.Create(CreateSuspended: Boolean); begin inherited Create(CreateSuspended); NameThreadForDebugging(ClassName, ThreadID); FQueue := TObjectList.Create(true); FcsQueue := TCriticalSection.Create; end; destructor TapDataReceiverQueue.Destroy; begin if Assigned(FQueue) then FreeAndNil(FQueue); if Assigned(FcsQueue) then FreeAndNil(FcsQueue); DataReceiverQueue := nil; inherited Destroy; end; procedure TapDataReceiverQueue.Execute; var QueueItem: TapDataReceiverQueueItem; begin while not Terminated do begin if (Count > 0) then begin if PERF_DATA_RECEIVER_THREADS_COUNT < PERF_DATA_RECEIVER_THREADS_MAX_VALUE then repeat try if Assigned(FQueue.Items[0]) then try QueueItem := TapDataReceiverQueueItem(FQueue.Items[0]); TapDataReceiver.Create(false, QueueItem.ClientData, QueueItem.Target, QueueItem.Priority); finally DeleteFromQueue(0); end; except end; until (Terminated) or (Count = 0) or (PERF_DATA_RECEIVER_THREADS_COUNT >= PERF_DATA_RECEIVER_THREADS_MAX_VALUE); end; Delay(1); end; end; procedure TapDataReceiverQueue.AddToQueue(AClientData: string; ATarget: TapDataTarget; APriority: TapPriority); var Item: TapDataReceiverQueueItem; begin Item := TapDataReceiverQueueItem.Create; Item.ClientData := AClientData; Item.Target := ATarget; Item.Priority := APriority; FcsQueue.Enter; try FQueue.Add(Item); finally FcsQueue.Leave; end; end; procedure TapDataReceiverQueue.DeleteFromQueue(AIndex: integer); begin FcsQueue.Enter; try FQueue.Delete(AIndex); finally FcsQueue.Leave; end; end; function TapDataReceiverQueue.Count: integer; begin Result := FQueue.Count; end; |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Ich habe ein ähnliches Szenario wie du und bin immer gut mit
Delphi-Quellcode:
klar gekommen.
TThread.Queue()
Soweit ich weiß legt Queue alles im Stack ab und dann wird der Stack nach und nach abgearbeitet. |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Delphi-Quellcode:
wird der Stack sequentiell verarbeitet.
TThread.Queue()
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Ok daran habe ich nicht gedacht.
Demnach bringen mehrere Threads mit derselben Aufgabe rein gar nicht, wenn man mit Queue alles in den Stack legt. Dann müssen wohl die Profis ran :stupid: Andererseits... du fügst doch nur Einträge in eine ObjectList hinzu. Die eigentliche Arbeit findet doch woanders statt. Dann sollte Queue doch eigentlich doch kein Problem sein. |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Zitat:
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Delphi-Quellcode:
an. Der ist genau für solche Zwecke gedacht. Du kannst dort einen
TThreadPool
Delphi-Quellcode:
einstellen und Tasks queuen. Der Pool arbeitet dann alle Aufgaben ab, aber erstellt nie mehr gleichzeitige Threads als angegeben (ist btw. auch deutlich performanter, da Thread Creation zumindest unter Windows einen ziemlichen Overhead hat und der ThreadPool die Threads wiederverwendet).
MaxThreadCount
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Es gibt ein s.g. vordefinierter DefaultPool. Von dem kann man die Zahl der Threads abrufen, die problemlos gleichzeitig laufen können. Gilt dieser Wert für das gesammte System oder nur für die aktuelle Anwendung? Ist es sinnvoll, einen eingenen ThreadPool zu erstellen? |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Delphi-Quellcode:
oder
AddToQueue()
Delphi-Quellcode:
absichern?
Count()
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Count ist doch nur lesend. Ich denke da muss man nix absichern.
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Zitat:
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
In meinem Fall hat die Verarbeitung der eingehenden Daten eine höhere Priorität, als andere zweitrangige Aufgaben, die sich eventuell erst während der Verarbeitung ergeben. Für diese zweitrangige Aufgaben existieren nähmlich auch Queue-Threads, die ihre Aufgaben mit einer niedriegeren Priorität abarbeiten. |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Das wird durch "enter" und "leave" sichergestellt. Mit der ThreadList müsstest Du dich nicht mehr darum zu kümmern zugriffe auf die Liste abzusichern. Das macht die Klasse dann automatisch. Grüße Klaus |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
A propos Zugriffe.
Muss man nur schreibende Zugriffe mit einer CriticalSection absichern oder auch lesende? |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Denn ich denke es wird krachen wenn ein Listenelemet gelesen wird und ein anderer Thread löscht im gleichen Zeitraum diese Element. Grüße Klaus |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Delphi-Quellcode:
nicht erklären können.
TMultipleReadExclusiveWriteSynchronizer
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Man muss alle gleichzeitigen Zugriffe absichern, die nicht atomar sind, sobald ein Prozess ggf. etwas schreibt.
Übrigens: auch ein
Delphi-Quellcode:
oder ein
I := J
Delphi-Quellcode:
ist - auf Prozessorebene - nicht atomar.
Inc(I)
Wenn man gemeinsame Zugriffe nicht korrekt absichert, kann man von falschen Werten bis Programmabstürzen alles erhalten. |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Zitat:
Delphi-Quellcode:
die Priorität des jeweiligen Queue-Threads anzupassen. Es gibt aber oft geteilte Meinung, dass dies auch nicht die beste Lösung ist.
TThreadPriority
Es hat mich auch interessiert, wie ich einem TTask meine Parameter übergeben kann. Dabei bin ich auf ![]()
Delphi-Quellcode:
übergeben werden. Muss das allgemein so sein?
const
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Wenn es wirklich eine Kopie ist, das mit dem Thread ja kein Problem mehr denn die ursprüngliche Variable wird nicht angefasst. |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Delphi-Quellcode:
ist tatsächlich nicht zwingend erforderlich, sondern nur eine Performance-Optimierung (
const
Delphi-Quellcode:
und
const
Delphi-Quellcode:
werden intern [meistens] als Pointer umgesetzt, wie a.def korrekterweise angemerkt hat). Im Falle von String bewirkt es vor allem, dass die automatische Referenzzählung deaktiviert und grade KEINE lokale Kopie erzeugt wird.
var
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
![]() Zitat:
Delphi-Quellcode:
weniger um den RefCounter ansich, sondern unnötige lokale Kopien und Speicheroperationen. Wenn man sich mal anschaut, wie eine leere Funktion mit String Parameter ohne
const S: String
Delphi-Quellcode:
oder
const
Delphi-Quellcode:
aussieht:
var
Code:
Im Vergleich zu einer Funktion mit
00822538 55 push ebp
00822539 8BEC mov ebp,esp 0082253B 51 push ecx 0082253C 8945FC mov [ebp-$04],eax 0082253F 8B45FC mov eax,[ebp-$04] 00822542 E82D79BEFF call @UStrAddRef 00822547 33C0 xor eax,eax 00822549 55 push ebp 0082254A 686B258200 push $0082256b 0082254F 64FF30 push dword ptr fs:[eax] 00822552 648920 mov fs:[eax],esp 00822555 33C0 xor eax,eax 00822557 5A pop edx 00822558 59 pop ecx 00822559 59 pop ecx 0082255A 648910 mov fs:[eax],edx 0082255D 6872258200 push $00822572 00822562 8D45FC lea eax,[ebp-$04] 00822565 E82678BEFF call @UStrClr 0082256A C3 ret 0082256B E9C86DBEFF jmp @HandleFinally 00822570 EBF0 jmp $00822562 00822572 59 pop ecx 00822573 5D pop ebp 00822574 C3 ret
Delphi-Quellcode:
bzw.
const
Delphi-Quellcode:
String-Parameter:
var
Code:
Hier sieht man ganz gut, dass man durch diese kleine Optimierung doch deutlich Overhead einspart.
00822578 55 push ebp
00822579 8BEC mov ebp,esp 0082257B 51 push ecx 0082257C 8945FC mov [ebp-$04],eax 0082257F 59 pop ecx 00822580 5D pop ebp 00822581 C3 ret |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Dass der Compiler so etwas nicht automatisch optimiert war mir immer schon ein Rätsel...
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zuerst möchte ich mich für die vielen hilfreichen Beiträge bedanken, habe wieder etwas dazu gelernt. Bevor ich nun die vielen nützlichen Vorschläge umsätze, möchte ich gerne zu meiner eigentlichen Frage zurückkehren: wäre mein geposteter Code so in Ordnung oder sollen die Zugriffe auf den Queue-Thread zusätzlich mit einer TCriticalSection abgesichert werden?
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Wenn ein anderer Thread TapDataReceiverQueue.AddToQueue aufruft musst Du natürlich im Thread
das:
Delphi-Quellcode:
absichern. Allerdings ACHTUNG bei Deinem eigenartigen Konstrukt. Da DeleteFromQueue() eine Prozedur von TapDataReceiverQueue ist UND Du dort schon die CriticalSection benutzt darfst Du diese nicht noch mal absichern. Ansonsten kann ein DEADLOCK entstehen. Also besser alle Methoden in TapDataReceiverQueue integrieren und mit Criticalsection absichern. Dann nie direkt auf die Liste gehen. So kannn nichts passieren.
try
if Assigned(FQueue.Items[0]) then try QueueItem := TapDataReceiverQueueItem(FQueue.Items[0]); TapDataReceiver.Create(false, QueueItem.ClientData, QueueItem.Target, QueueItem.Priority); finally DeleteFromQueue(0); end; Ergänzung: Das TapDataReceiverQueue.Count natürlich auch. |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Zitat:
Delphi-Quellcode:
wird nur aus dem vom Thread selbst aufgerufen. Public sind nur
DeleteFromQueue()
Delphi-Quellcode:
und
AddToQueue()
Delphi-Quellcode:
. In beiden sind die Zugriffe auf die TObjectList geschützt (für Count nachträglich). Andere Methoden gibt es nicht. Was ist gemeint mit "alle Methoden in TapDataReceiverQueue integrieren und mit Criticalsection absichern"?
Count()
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Delphi-Quellcode:
Das bedeutet Du mischst Zugriffe auf die Lokale Liste der Klasse (ohne Absicherung) mit Methoden der Klasse (inkl. Absicherung).
try
if Assigned(FQueue.Items[0]) then // --> hier ist ein direkter Zugriff auf die "interne" Liste ohne Absicherung try QueueItem := TapDataReceiverQueueItem(FQueue.Items[0]); // hier ebenfalls TapDataReceiver.Create(false, QueueItem.ClientData, QueueItem.Target, QueueItem.Priority); // was ist eigentlich das hier? finally DeleteFromQueue(0); // hier ist der Zugriff über eine Methode der Klasse mit Absicherung end; Meiner Meinung nach solltest Du die Klasse
Delphi-Quellcode:
so bauen dass Du aus dem Thread alles mit Klassenmethoden machen kannst (besser MUSST). Zum Beispiel eine Methode Pop um die Objekte zu holen. In der Klasse sicherst Du alles mit der CriticalSection ab. Somit muss sich kein Aufrufer der Methoden um etwas kümmern. Egal aus welchem Thread. Dafür würde sich übrigens die Generische TQueue ev. besser eignen als die TObjectlist.
TapDataReceiverQueue
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Hmm..
Also: Generell MUSS jeder Zugriff von verschiedenen Threads auf Variablen gekapselt werden. Dabei ist es egal ob auf die Variable von externen Threads oder vom eigenen Thread darauf zugegriffen wird. Es sind alle konkurierende Threads. Somit müssen 'Add' 'Count' 'Delete' 'Extract' 'READ' und alle eventuell weiteren Zugriffe auf deine Liste gekapselt werden! |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
Heißt kapseln, dass CriticalSection Enter und Leave bzw TThread.Queue() reicht?
|
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
![]() Oder die ganzen TInterlocked .... Methoden für einfache Variablenzugriffe. Rollo |
AW: Zugriffe auf Objekt aus mehreren Threads - wie richrig synchronisieren?
In Post #5 wurde vorgeschlagen statt TObjectList eine TThreadlist zu verwenden. Dieser Vorschlag ist auch deshalb sehr klug, weil Du dann mehr oder weniger gezwungen wirst alles richtig zu machen. TThreadlist zwingt Dich den Zugriff auf die Liste abzusichern.
Deine Konstruktion
Delphi-Quellcode:
ist unzulässig, weil Du dabei davon ausgehst, dass sich FQueue.Items während Deines Zugriffs nicht verändert. Das ist aber keine zugesicherte Eigenschaft. Ein anderer Thread kann, während Du FQueue.Items benutzt, eine neue Aufgabe zur Queue hinzufügen. Dadurch könnte sich potentiell die Adresse von FQueue.Items ändern. Um das zu verhindern musst Du also vor dem Zugriff auf FQueue die Queue locken.
if Assigned(FQueue.Items[0]) then
try QueueItem := TapDataReceiverQueueItem(FQueue.Items[0]); TapDataReceiver.Create(false, QueueItem.ClientData, QueueItem.Target, QueueItem.Priority); finally DeleteFromQueue(0); end; Wenn Du TThreadlist benutzt, würde der Code etwa so aussehen:
Delphi-Quellcode:
Dabei ist zu beachten, dass man niemals auf LList außerhalb des Schutzblockes zugreift. Also nicht so
var
LList: TList; begin ... QueueItem := nil; try LList := FQueue.LockList; try // Pop... if LList.Count>0 then begin QueueItem := TapDataReceiverQueueItem(LList.Items[0]); LList.Delete(0); end; finally FQueue.UnlockList; end; if Assigned(QueueItem) then TapDataReceiver.Create(false, QueueItem.ClientData, QueueItem.Target, QueueItem.Priority); finally QueueItem.Free; end;
Delphi-Quellcode:
Dadurch wird die Sperrung der Liste ausgehebelt und das Ganze verhält sich wieder wie TObjectList.
LList := FQueue.LockList;
FQueue.Unlocklist; if LList.Count>0 then... |
Alle Zeitangaben in WEZ +1. Es ist jetzt 07:47 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