ore12 hat geschrieben:Danke für Eure Hinweise! Ich hatte vor, vor dem Start des Threads ein paar Parameter reinzuladen...
Allgemein: Manged-Typen (Ansistrings, UnicodeStrings, Dynamische Arrays, Interfaces mit automatischer Referenzzählung) müssen vor Threadstart eine eigene Referenz erhalten. Während der Thread läuft, darf auf die Variable nur durch den Thread selbst oder in geschütztem Code (Critical Section) zugegriffen werden. Selbst Lesezugriff aus anderen Threads sind verboten, wenn der Thread die Variable ändern kann.
Eine Frage habe ich aber dazu: warum darf man von aussen keine Lesezugriffe machen auf Variablen, die nur der Thread selbst schreibt?
Was kann da passieren?
Nur auf managed typen, also Typen die Referenzgezählt sind. Das liegt daran das bei jeder Zuweisung (und ein lesender zugriff kann man sich als zuweisung auf ein temporäres objekt vorstellen) aus 3 schritten besteht: 1. Referenzzähler (vom zu überschreibenden objekt) dekrementieren und objekt eventuell freigeben, 2. Referenz überschreiben 3. Referenzzähler (vom neuen objekt) erhöhen.
Sagen wir du liest das objekt von Thread1 in Thread2 während gleichzeitig Thread1 das objekt überschreibt. Dann kann die Referenzzahl dekrementiert werden bevor Thread2 sie inkrementiert, was dazu führen kann das Thread1 das Objekt löscht bevor er wissen kann das Thread2 es besitzt.
So genannte Race conditions sind aber nicht nur bei gemanageten typen problematisch. Beispiel Integer:
Code: Alles auswählen
// Thread1:
self.x := self.x + 1; // (1)
// Thread2:
Thread1.x := 0; // (2)
(1) besteht aus 3 operationen: 1. lesen, 2. Inkrementieren, 3. schreiben. Wenn zwischen 1 und 3 die operation aus (2) ausgeführt wird, sieht das so aus:
Code: Alles auswählen
x |operation
n |lese x (thread1)
0 |schreibe x (thread2)
n+1| schreibe x+1 (thread1)
Somit wird die operation von Thread2 einfach "übersprungen" weil Thread1 die änderung nicht mitbekommt.
Nächstes beispiel: Composite typen (records):
Code: Alles auswählen
r: record
a, b, c: Integer;
end;
// Thread1:
self.r := someRecord;
// Thread2:
Thread1.r := someOtherRecord;
Das wird dann praktisch zu sowas:
Code: Alles auswählen
// Thread1:
self.r.a := someRecord.a;
self.r.b := someRecord.b;
self.r.c := someRecord.c;
// Thread2:
Thread1.r.a := someOtherRecord.a;
Thread1.r.b := someOtherRecord.b;
Thread1.r.c := someOtherRecord.c;
Je nachdem wie die Threads gescheduled werden (also in welcher reihenfolge die ausgeführt werden) kann da alles bei rauskommen, z.B. kann am ende sowas drinstehen:
Code: Alles auswählen
r.a := someOtherRecord.a;
r.b := someRecord.b;
r.c := someOtherRecord.c;
Somit hat man ein gemische von daten, die zu zwei komplett unterschiedlichen Ursprüngen gehören. Bei Composite typen kann das auslesen auch probleme machen:
Code: Alles auswählen
// Thread1:
self.r.a := someRecord.a;
self.r.b := someRecord.b;
self.r.c := someRecord.c;
// Thread2:
someOtherRecord.a := Thread1.r.a;
someOtherRecord.b := Thread1.r.b;
someOtherRecord.c := Thread1.r.c;
Jetzt kann in someOtherRecord eine mischung aus dem ursprünglichen Inhalt von Thread1.r und dem neuen inhalt drinstehen.
Langer rede kurzer Sinn, wenn du dir nicht ziemlich sicher bist das es kein Problem gibt (z.b. Booleans sind sicher, Integer nur wenn man keine nichtatomaren funktionen benutzt, Gemanagete typen nie, und records nur wenn es nur aus atomaren feldern besteht die nur einzeln zugegriffen werden) immer locken.
Aber locks immer möglichst klein halten sonst kannst du deadlocks bekommen (also das zwei locks aufeinander warten). Grundsätzlich gilt: Threading ist gar nicht mal so einfach, und man sollte Speicherzugriffe zwischen Threads wenn möglich immer verzichten
Kleines Beispiel:
Code: Alles auswählen
program Project1;
{$mode objfpc}{$H+}
uses {$IFDEF UNIX}
cthreads, {$ENDIF}
Classes;
var
x: integer;
type
TTestThread = class(TThread)
protected
procedure Execute; override;
end;
procedure TTestThread.Execute;
var
i: integer;
begin
for i := 1 to 1000000 do
Inc(x);
end;
var
t1: TTestThread;
t2: TTestThread;
begin
t1 := TTestThread.Create(False);
t2 := TTestThread.Create(False);
t1.WaitFor;
t2.WaitFor;
WriteLn('X: ', x);
ReadLn;
end.
Eigentlich wird x genau 2 millionen mal inkrementiert, das ergebniss ist aber (sehr wahrscheinlich) kleiner als 2 mio, wegen den oben erklärten phenomena. Wenn man in diesem beispiel statt der nicht thread-safen funktion Inc(x), die Thread-Safe funktion InterLockedIncrement(x) benutzt, funktioniert es wunderbar und es kommt 2 mio raus