Exception Message: Thread was created from extern.

Für Fragen zur Programmiersprache auf welcher Lazarus aufbaut
Antworten
jds
Beiträge: 3
Registriert: Di 9. Jan 2018, 02:25
OS, Lazarus, FPC: Debian GNU/Linux (FPC 3.0.4 (stable-backports))
CPU-Target: x86_64

Exception Message: Thread was created from extern.

Beitrag von jds »

Hi und Hallo,
ich verwende FPC 3.0.4 aus Debian stable-backports auf der Kommandozeile.
Mein Problem ist jetzt folgendes:
Ich habe ein Programm, dass ziemlich rabiates Numbercrunching macht. Diese Operationen habe ich erfolgreich auf zwei Threads verteilt, dann stellte ich fest, dass es ab einer gewissen Menge an Operanden sinnvoll ist, wenn der Workerthread sich noch einen Helperthread erstellt und dessen Ergebnisse zu seinen hinzufügt. Auch dass lief, solange die beiden Klassen im Source des Hauptprogramms standen. Da das Programm mittlerweile sehr länglich geraten ist, habe ich die funktionierenden Klassen in eine Unit ausgelagert.

Das Programm compiliert und startet, bricht aber mit einer Exception mit obiger Message ab. In der Folge ein Minimalbeispiel:

Code: Alles auswählen

 
{$mode objfpc}
unit mythreads;
interface
uses
  {$ifdef unix}
    cthreads,
    cmem,
  {$endif}
    classes;
  (* hier kaemen noch selbstgeschriebene Units fuer das Numbercrunching *)
 
type
  THelperThread = class(TThread)
  protected
    procedure Execute; override;
  end;
 
  TWorkerThread = class(TThread)
  protected
    procedure Execute; override;
  end;
 
implementation
 
  procedure THelperThread.Execute;
   begin
     write(chr(10));
     Terminate;
  end;
 
  procedure TWorkerThread.Execute;
  var
    lc : Byte;
    ht : THelperThread;
  begin
     ht := THelperThread.Create(true);
     for lc := 1 to 128 do
     begin
       write(lc:3);
       ht.Start;
       ht.WaitFor;
     end;
     ht.Destroy;
     Terminate;
  end;
 
End.
 


Und jetzt der Paarzeiler mit dem eingentlichen Programm:

Code: Alles auswählen

 
{$Mode objfpc}
program threadtst;
uses
  {$ifdef unix}
     cthreads,
     cmem,
  {$endif}
     sysutils,
     mythreads;
 
var
  wt : TWorkerThread;
 
Begin
  try
    wt := TWorkerThread.Create(false);
    wt.WaitFor;
    wt.Destroy;
  except
    on e : Exception do
      writeln(e.message);
  end;
End.
 


Ich bedanke mich bereits vorab für mögliche Vorschläge.

Gruss,
jds

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: Exception Message: Thread was created from extern.

Beitrag von Socke »

Dein Programm erstellt einen Thread, lässt diesen laufen und starten ihn anschließen neu.
Soweit ich die interne Funktion ThreadProc in der Unit Classes (classes.inc) richtig interpretiere, ist dies nicht beabsichtigt.

Unter Windows mit FPC 3.0.2 wird der Helper-Thread nur ein einziges Mal durchlaufen. Die Programmausgabe ist:

Code: Alles auswählen

C:\Temp>project1.exe
  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 2
8 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
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 9910010110210310410510610710
8109110111112113114115116117118119120121122123124125126127128


Folgende Methode erzeugt pro Thread eine Zeile:

Code: Alles auswählen

procedure TWorkerThread.Execute;
  var
    lc : Byte;
    ht : THelperThread;
  begin
     for lc := 1 to 128 do
     begin
       ht := THelperThread.Create(true);
       write(lc:3);
       ht.Start;
       ht.WaitFor;
       ht.Destroy;
     end;
     Terminate;
  end;


Wenn du Threads wiederverwenden möchtest, dürfen diese nicht zum Ende der Prozedur Execute kommen. Stattdessen sollten Sie auf ein Event warten und dann aus einer Queue oder Variablen des eigenen Thread-Objektes eine neue Aufgabe entgegennehmen.
Ein Beispiel dazu findest du in meinem Package: https://github.com/SAmeis/pascal-futures/blob/master/src/futures.pas; relevant sind die Methode TThreadFutureList.NotifyThreads und die Klasse TFutureManager.TWorkerThread. Das Event um neue Aufgaben anzuzeigen wird in der Methode TFutureManager.TWorkerThread.AfterCreate angelegt, diese wird nach dem Konstruktor aufgerufen, aber bevor der Thread gestartet wird.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

jds
Beiträge: 3
Registriert: Di 9. Jan 2018, 02:25
OS, Lazarus, FPC: Debian GNU/Linux (FPC 3.0.4 (stable-backports))
CPU-Target: x86_64

Re: Exception Message: Thread was created from extern.

Beitrag von jds »

Hi und Hallo,
danke Socke.
Ich muss zugeben, dass mein Minimalbeispiel an Vorführeffekt leidet. Es reproduziert die gewünschte Exception nicht,
Bis zu zu dem Punkt an dem die Daten in den Helperthread geladen werden kommt das Programm nicht, es bleibt hängen, wenn der Workerthread den Helperthread erstellt.

Ich gebe hier dann Ausschnitte aus meinem Originalcode.

Code: Alles auswählen

 
procedure TApproxThread.Execute;
var
  p1, p2          : TNumber;
  dividends, pass : TNumArray;
  ic              : TDynArrayIndex;
  actual          : TApprox;
  HT1             : TDivideHelperThread;
begin
  setlength(approxes, 0);
  for p1 := range_low to range_high do
  begin
    setlength(dividends, 0);
    setlength(pass, 0);
    if (atkintest(p1)) then
    begin
      actual.devisor := p1;
      for p2 := floor64(3.1*actual.devisor) to floor64(3.2*actual.devisor) do
      begin
        setlength(dividends,(Length(dividends)+1));
        dividends[High(dividends)] := p2;
      end;
 
      if( 2000 <= Length(dividends)) then
      begin
        for ic := Low(dividends) to (Length(dividends) div 2) do
        begin
          setlength(pass,(Length(pass)+1));
          pass[High(pass)] := dividends[ic];
        end;
        HT1 := TDivideHelperThread.Create(true);
        HT1.SetParams(actual.devisor, pass);
        HT1.Start;
        for ic := (ic+1) to High(dividends) do
        begin
          if(atkintest(dividends[ic])) then
          begin
            actual.dividend := dividends[ic];
            actual.value := actual.dividend/actual.devisor;
            actual.offset := actual.value - pi;
            if (0.0001 >= abs(actual.offset)) then
            begin
              setlength(approxes, (Length(approxes)+1));
              approxes[High(approxes)] := actual;
            end;
            if (3.16 < actual.value) then
            begin
              break;
            end;
          end;
        end;
        repeat
          self.sleep(333);
        until(HT1.finished);
 
        approxes := concat(approxes, HT1.approximations);
        HT1.Destroy;
      end
      else
      begin
        for p2 in dividends do
        begin
          if(atkintest(p2)) then
          begin
            actual.dividend := p2;
            actual.value := actual.dividend/actual.devisor;
            actual.offset := actual.value - pi;
            if (0.0001 >= abs(actual.offset)) then
            begin
                setlength(approxes, (Length(approxes)+1));
              approxes[High(approxes)] := actual;
            end;
          end;
        end;
      end;
    end;
  end;
  Terminate;
end;
 


Und hier noch die Execute-Methode des Helperthread:

Code: Alles auswählen

 
procedure TDivideHelperThread.Execute;
var
  lc     : TNumber;
  actual : TApprox;
begin
 
  actual.devisor := divisor;
 
  for lc in dividends do
  begin
    if (atkintest(lc)) then
    begin
      actual.dividend := lc;
      actual.value := actual.dividend/actual.devisor;
      actual.offset := actual.value - pi;
      if (0.0001 >= abs(actual.offset)) then
      begin
        setlength(approxes, (Length(approxes)+1));
        approxes[High(approxes)] := actual;
      end;
      if (3.15 < actual.value) then
      begin
        break;
      end;
    end;
  end;
  Terminate;
end;
 


Solange die Klassen mit dem Hauptprogramm in einer Datei stehen klappt alles, erst nach der Auslagerung geht es schief.

Schonmal Dank vorab.

Mit bestem Gruss.

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: Exception Message: Thread was created from extern.

Beitrag von Socke »

Anstatt ein einer Schleife zu warten, bis der Helper Thread fertig ist, solltest du mit HT1.WaitFor(); den TApproxThread schlafen legen, das ist effizienter.

Weiterhin änderst du den Array approxes in beiden Threads, hier gibt es gleich mehrere Probleme:
  • Der Array wird jedes Mal um ein Element vergrößert. Das führt zu Speicherfragmentierung und langsamen Programmen, erzeugt aber grundsätzliche kein Problem in der Anwendung.
  • Der Array wird in beiden Threads vergrößert. Das Vergrößern selbst ist unproblematisch, da die Prozedur SetLength threadsafe ist. Das Auslesen der aktuellen und Berechnen der neuen Größe ist aber nicht threadsafe. Damit liest bspw. TApproxThread die Länge 5, berechnet die neue Größe 6. Dann kommt TDivideHelperThread und vergrößert den Array auf 6 Elemente. Erst dann kommt wieder TApproxThread und setzt die Array-Länge auf die berechneten 6 Elemente.
  • Der Inhalt des Arrays wird vollkommen ohne threadsicherung verändert.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

jds
Beiträge: 3
Registriert: Di 9. Jan 2018, 02:25
OS, Lazarus, FPC: Debian GNU/Linux (FPC 3.0.4 (stable-backports))
CPU-Target: x86_64

Re: Exception Message: Thread was created from extern.

Beitrag von jds »

Hi und Hallo,
danke Socke.
Das Klassendesign, wie ich es gepostet hatte, ist Schrott gewesen. Jede Threadklasse hatte ihr eigenes Array und die Entsprechende property hatte den selben Namen.
Zusammengeführt wurden beide Arrays im Hauptthread in der Zeile:

Code: Alles auswählen

 
approxes := concat(self.approxes, HT1.approxes);
 


Aber dies ist jetzt Geschichte.
Ich habe die Klasse komplett neu entworfen, um die Ideen aus deiner ersten Antwort aufzunehmen.
Aktuell sieht die Klasse jetzt so aus. Für mein konkretes Problem leite ich einen Spezialisten ab, der
die Prozedur filter implementiert.

Code: Alles auswählen

 
 TApproxThread = class(TThread)
  private
    results : TApproxArray;
    procedure filter(candidate: Extended); abstract;
  protected
    procedure Execute; override;
  public
    procedure DoDivides( ldividend, hdividend, divisor: TNumber );
    constructor Create(CreateSuspended: Boolean);
  published
    approximations : TApproxArray read results;
  end;
 


So jetzt ergeben sich für mich folgende Fragen:
Ich kann meine Helperthreadgeschichte in zwei Varianten erledigen:
    ich instanziere einen Subthread, was wahrscheinlich wieder zu einer Exception zur Runtime führen wird (EThreadExternalException?).
    ich schicke DoDivides in einen temporären Thread mittels ExecuteInThread.

Bei Variante zwei stellt sich dann die Frage, wie komme ich an die Ergebnisse bzw. wo schreibt der Tempthread diese hin?
Ich vermute, dass das auf ein TNotifyEvent hinausläuft, wobei mit die Arbeitsweise noch nicht ganz klar wird. Die Delphi-Beispiele, welche ich gesehen habe waren nicht sonderlich erhellend.

Gruss,
jds

Antworten