Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   Delphi Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn! (https://www.delphipraxis.net/176205-exception-ohne-wirklichen-ausloeser-treibt-mich-den-wahnsinn.html)

berens 19. Aug 2013 15:38

Delphi-Version: 2007

Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Hallo mal wieder.

Ich bin mit meinem Latei mal wieder komplett am Ende, und selbst die SuFu oder Google liefern mir hier noch nicht mal mehr Ansätze.

Ich habe hier (m)ein Programm, das aus einer Datenbank über einen Thread Werte lädt, diese mittels Synchronize() an eine Komponente aus dem Hauptformular übergibt und dann weiterverarbeitet wird. Das klappt alles 100% zuverlässig und störungsfrei.

Nun ergibt sich aber das Problem, dass, sobald ich/der Benutzer etwas in ein TEdit oder TMemo eingibt, und (nach wilden rumtippseln auf der Tastatur) dann Leertaste drücke, eine Exception kommt. Alle TEdit/TMemo Objekte haben kein On-Ereignis hinterlegt. Alle Formulare haben KeyPreview auf "False". Es scheint so, als ob beim Drücken der Leertaste Delphi/Windows intern "Application.ProcessMessages" aufruft (siehe Stack weiter unten) und dann irgendwas im Hintergrund schafft.

Ich vermute ja fast, dass Delphi alle Nachfahren von TObject, die momentan als Variable in meinem Programm/Speicher vorhanden sind benachrichtigt, dass der Hotkey "Leertaste" gedrückt wurde. Interessiert zwar keine einzige Komponente, aber egal. Während die Liste (wo nimmt er die her?) mit allen Komponenten durchgeht, wird wohl eine Komponente aus der Liste gelöscht, und es kommt zu der Exception. Meine Vermutung.

Der Thread löscht keine Komponenten aus dem Hauptprogramm. Beim Debuggen wird mir beim Anklicken aller Threads nacheinander bei "Thread-Status" auch kein Thread angezeigt, in dem aktuell mein Quelltext läuft oder sonst irgendwie etwas gelöscht wird (es gibt ja immer mehrere "leere" Threads beim Debuggen wo nur "WaitForSingleObject" o.ä. als einziger Stackbefehl zu sehen ist). Der VCL/Main Thread ist idle und durchläuft zu diesem Zeitpunkt keine Prozeduren von mir.

Ich verwende außer meinen Objekten und den Standard-Komponenten nur ein paar LMD-Panels etc., aber die werden nur bei Programmstart erzeugt. Keine weiteren Threads oder Fremdkomponenten.

Wie bekomme ich denn raus, bei welcher TList der .Get-Befehl fehlgeschlagen ist? Was kann man machen? Kann ich der TApplication die Reaktionen auf Tastatur-Anschläge verbieten (MessageHandler überschreiben, ...)?


Anbei das Log:
Code:
EurekaLog 7.0.6.0 

Exception:
-------------------------------------------------------------
  2.2 Address: 00436D20
  2.5 Type  : EListError
  2.6 Message: Listenindex überschreitet das Maximum (21116).
  2.7 ID    : 60500000
  2.11 Sent : 0

User:
-----------------
  3.2 Name : name
  3.3 Email:

Steps to reproduce:
------------
  8.1 Text:


Call Stack Information:
-------------------------------------------------------------------------------------------------------------------------------------------------
|Methods |Details|Stack  |Address |Module     |Offset |Unit            |Class         |Procedure/Method                        |Line     |
-------------------------------------------------------------------------------------------------------------------------------------------------
|*Exception Thread: ID=4024; Parent=0; Priority=0                                                                                               |
|Class=; Name=MAIN                                                                                                                             |
|DeadLock=0; Wait Chain=                                                                                                                       |
|Comment=                                                                                                                                      |
|-----------------------------------------------------------------------------------------------------------------------------------------------|
|7FFFFFFE|04     |00000000|00436D20|pCRM7.exe  |00036D20|Classes         |TList         |Get                                     |2992[2]  |
|00000020|04     |0018FBA8|00440ABB|pCRM7.exe  |00040ABB|Classes         |TComponent    |GetComponent                            |10673[2] |
|00000020|04     |0018FBB4|005034BF|pCRM7.exe  |001034BF|Forms           |               |DispatchShortCut                        |5997[5]  |
|00000020|04     |0018FBD0|0050356D|pCRM7.exe  |0010356D|Forms           |TCustomForm   |IsShortCut                              |6018[3]  |
|00000020|03     |0018FEB8|75BE6175|USER32.dll |00026175|USER32           |               |SendMessageA                            |          |
|00000020|04     |0018FEDC|005067EB|pCRM7.exe  |001067EB|Forms           |TApplication  |IsKeyMsg                                |8022[24] |
|00000020|04     |0018FF08|00506A49|pCRM7.exe  |00106A49|Forms           |TApplication  |ProcessMessage                          |8097[15] |
|00000020|04     |0018FF20|00506AAE|pCRM7.exe  |00106AAE|Forms           |TApplication  |HandleMessage                           |8124[1]  |
|00000020|04     |0018FF44|00506DA3|pCRM7.exe  |00106DA3|Forms           |TApplication  |Run                                     |8223[20] |
|00000020|04     |0018FF74|006B05B5|pCRM7.exe  |002B05B5|pCRM7            |               |Initialization                          |39[8]    |
|00000020|03     |0018FFD8|77BD9F40|ntdll.dll  |00039F40|ntdll           |               |(possible RtlInitializeExceptionChain+49)|          |
|-----------------------------------------------------------------------------------------------------------------------------------------------|
|                                                                                                                                               |
|Running Thread: ID=5216; Parent=4024; Priority=0                                                                                               |
|Class=TLoadSaveThread; Name= (uLoadSave.TLoadSaveThread.Execute)                                                                              |
|DeadLock=0; Wait Chain=thread: [ 1460 / 5216 ] is blocked                                                                                     |
|Comment=                                                                                                                                      |
|-----------------------------------------------------------------------------------------------------------------------------------------------|
|7FFFFFFE|03     |00000000|77BBF8D1|ntdll.dll  |0001F8D1|ntdll           |               |ZwWaitForSingleObject                   |          |
|00000020|03     |03F3F984|766F118F|kernel32.dll|0001118F|kernel32         |               |WaitForSingleObjectEx                   |          |
|00000020|03     |03F3F99C|766F1143|kernel32.dll|00011143|kernel32         |               |WaitForSingleObject                     |          |
|00000020|04     |03F3F9B0|0044028F|pCRM7.exe  |0004028F|Classes         |TThread       |Synchronize                             |10168[34] |
|00000020|04     |03F3FA00|00440336|pCRM7.exe  |00040336|Classes         |TThread       |Synchronize                             |10196[4] |
|00000020|04     |03F3FA08|006942DE|pCRM7.exe  |002942DE|uLoadSave       |TLoadSaveThread|Execute                                 |1157[644] |
|00000020|04     |03F3FE64|004DE045|pCRM7.exe  |000DE045|EAppMultiThreaded|               |ThreadExecuteSafeWrapper                |64[11]   |
|00000020|04     |03F3FEA8|0043FE10|pCRM7.exe  |0003FE10|Classes         |               |ThreadProc                              |9877[7]  |
|00000020|04     |03F3FED8|00405334|pCRM7.exe  |00005334|System          |               |ThreadWrapper                           |12110[33] |
|00000020|04     |03F3FEEC|004DE21E|pCRM7.exe  |000DE21E|EAppMultiThreaded|               |ThreadProcHandler                       |137[13]  |
|00000020|04     |03F3FF34|004C2607|pCRM7.exe  |000C2607|EExceptionManager|               |DefaultThreadHandleException            |2850[3]  |
|00000020|04     |03F3FF78|0049D8C3|pCRM7.exe  |0009D8C3|EThreadsManager |               |ThreadWrapper                           |611[11]  |
|00000020|03     |03F3FF8C|766F33A8|kernel32.dll|000133A8|kernel32         |               |BaseThreadInitThunk                     |          |
|7FFFFFFE|04     |00000000|0069135B|pCRM7.exe  |0029135B|uLoadSave       |TLoadSaveThread|Create                                  |121[3]   |
-------------------------------------------------------------------------------------------------------------------------------------------------

Processes Information:
----------------------


Assembler Information:
-----------------------------------------------------------
; Classes.TList.Get (Line=2991 - Offset=9)
; ----------------------------------------
00436D15  add    eax, +$7C08733B
;
; Line=2991 - Offset=14
; ---------------------
00436D1A jnp    +$6C414415
;
; Line=2992 - Offset=20
; ---------------------
00436D20  add    [ebx+$E8038BCE], cl      ; <-- EXCEPTION
;
; Line=2992 - Offset=26
; ---------------------
00436D26  push   ds
;
; Line=2992 - Offset=27
; ---------------------
00436D27  DB     $FF, $FF //     
;
; Line=2992 - Offset=29
; ---------------------
00436D29  dec    dword ptr [ebx+$48B0443]
;
; Line=2993 - Offset=35
; ---------------------
00436D2F mov    al, $5E
;
; Line=2994 - Offset=37
; ---------------------
00436D31  pop    ebx
;
; Line=2994 - Offset=38
; ---------------------
00436D32  ret

Registers:
-----------------------------
EAX: 0018FAC4   EDI: 00000001
EBX: 00000000   ESI: 0EEDFADE
ECX: 00000007   EBP: 0018FB14
EDX: 00000000   ESP: 0018FAC4
EIP: 7583C41F  FLG: 00000212
EXP: 00436D20   STK: 0018FB9C

Stack:              Memory Dump:
------------------   ---------------------------------------------------------------------------
0018FBD8: 00000000   00436D20: 00 8B CE 8B 03 E8 1E FF FF FF 8B 43 04 8B 04 B0  ...........C....
0018FBD4: 0018FBEC  00436D30: 5E 5B C3 90 8B 50 0C 83 FA 40 7E 0E 8B CA 85 C9  ^[...P...@~.....
0018FBD0: 00503572   00436D40: 79 03 83 C1 03 C1 F9 02 EB 11 83 FA 08 7E 07 B9  y............~..
0018FBCC: 0018FBEC  00436D50: 10 00 00 00 EB 05 B9 04 00 00 00 03 CA 8B D1 E8  ................
0018FBC8: 02742020   00436D60: 64 01 00 00 C3 8D 40 00 53 33 C9 EB 01 41 3B 48  d.....@.S3...A;H
0018FBC4: 00000018   00436D70: 08 7D 08 8B 58 04 3B 14 8B 75 F2 3B 48 08 75 03  .}..X.;..u.;H.u.
0018FBC0: 02742020   00436D80: 83 C9 FF 8B C1 5B C3 90 53 56 57 8B F9 8B F2 8B .....[..SVW.....
0018FBBC: 00503514   00436D90: D8 85 F6 7C 05 3B 73 08 7E 0F 8B 15 44 41 6C 00  ...|.;s.~...DAl.
0018FBB8: 00000000   00436DA0: 8B CE 8B 03 E8 9F FE FF FF 8B 43 08 3B 43 0C 75  ..........C.;C.u
0018FBB4: 005034C4   00436DB0: 06 8B C3 8B 10 FF 12 8B 43 08 3B F0 7D 1A 2B C6  ........C.;.}.+.
0018FBB0: 0ABF0580   00436DC0: 8B C8 03 C9 03 C9 8B 43 04 8D 54 B0 04 8B 43 04  .......C..T...C.
0018FBAC: 00000001   00436DD0: 8D 04 B0 E8 A4 C6 FC FF 8B 43 04 89 3C B0 FF 43  .........C..<..C
0018FBA8: 00440AC0   00436DE0: 08 85 FF 74 0B 33 C9 8B D7 8B C3 8B 18 FF 53 04  ...t.3........S.
0018FBA4: 02742020   00436DF0: 5F 5E 5B C3 8B 50 08 4A E8 0F FF FF FF C3 8B C0  _^[..P.J........
0018FBA0: 0000527C  00436E00: 53 56 57 55 8B F1 8B FA 8B D8 3B F7 74 45 85 F6  SVWU......;.tE..
0018FB9C: 00436D2A  00436E10: 7C 05 3B 73 08 7C 0F 8B 15 44 41 6C 00 8B CE 8B |.;s.|...DAl....

Union 19. Aug 2013 16:58

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Was machst Du denn im Execute des Threads ausserhalb der Synchronize-Aufrufe?

berens 19. Aug 2013 21:10

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von Union (Beitrag 1225365)
Was machst Du denn im Execute des Threads ausserhalb der Synchronize-Aufrufe?

Dadurch, dass ich leider auf eine Access-Datenbank über Netzlaufwerk über <2MBit VPN angewiesen bin, lade ich alle Datensätze über einen Thread direkt in Objekte. Die Objekte werden dann von den Main-Thread kopiert (neu erzeugen und .Assign)


Beispiel:
Laden der Daten aus Datenbank
Delphi-Quellcode:
  TProjekt = class(TLoadSaveItem)
  public
    txtProjektName: string;
    dtEndDatum: TDateTime;
    dtStartDatum: TDateTime;
    blAusblenden: Boolean;
    function Equals(_CompareTo: TProjekt): Boolean;
    procedure Assign(_Source: TPersistent); override;
    constructor Create(_Owner: TComponent); override;
  end;


            while (not q.Eof) and (not FblStop) and (ToDo = 0) do begin
              inc(intCounter);
//              OutputDebugString(pchar('LoadSave.Laden.Projekte.Counter: ' + inttostr(intCounter)));

              tmpProjekt := TProjekt.Create(NIL);
              with tmpProjekt do begin
                ID := q.FieldByName('ID').AsInteger;
                dtEndDatum := floor(q.FieldByName('dtEndDatum').AsDateTime);
                dtStartDatum := floor(q.FieldByName('dtStartDatum').AsDateTime);
                txtProjektName := q.FieldByName('txtProjektName').AsString;
                blAusblenden := q.FieldByName('blAusblenden').AsBoolean;
                LastUpdate := CurrentTick;
              end;

              // Genau dieses Projekt in der Liste der bereits geladenen Projekte suchen
              blGefunden := False;
              for i := 0 to FclProjekte.Count - 1 do begin
                if FclProjekte.Items[i].InheritsFrom(TProjekt) then begin
                  ListenProjekt := TProjekt(FclProjekte.Items[i]);
                  // Projekt gefunden
                  if ListenProjekt.ID = tmpProjekt.ID then begin
                    ListenProjekt.Assign(tmpProjekt);
                    blGefunden := True;
                    Break;
                  end;
                end;
              end;

              if blGefunden then begin
                FreeAndNil(tmpProjekt);
              end else begin
                Synchronize(SyncNewDataset);
                FclProjekte.Add(tmpProjekt);
              end;

              q.Next;
              Synchronize(SyncSaveAndDelete); // damit auch beim Laden abgebrochen werden kann

              if (intCounter mod 10 = 0) then begin
                FProgressText := Format('%s von %s Projekten geladen', [IntToStr(intCounter), IntToStr(q.RecordCount)]);
                Synchronize(SyncProgress);
              end;
            end;

            q.Close;
            q.SQL.Clear;

            try
              i := 0;
              // Alle Einträge durchgehen
              while i < FclProjekte.Count do begin

                // Wenn Eintrag in der Liste nicht beim aktuellen Neu-Lade Zyklus mit dabei war
                tmpLS := TLoadSaveItem(FclProjekte.Items[i]);
                if tmpLS.LastUpdate <> CurrentTick then begin
                  FclProjekte.Delete(i);
                end else begin
                  inc(i);
                end;
              end;
            except end;

            Synchronize(SyncProjekte);
            FProgressText := Format('%s Projekte geladen', [IntToStr(intCounter)]);
            Synchronize(SyncProgress);
          end;
Und dann mit Synchronize die in Objekte aus der THREAD-Componentliste (FclProjekte) in die VCL (Main-Thread)-Componentenliste (clProjekte) kopieren mittels assign.

Sollte ja nix ausmachen, dass die clProjekte ein Objekt des Threads ist? Schließlich wird ja mit .Execute NIE direkt darauf zugegriffen...
Delphi-Quellcode:
{$Region 'procedure TLoadSaveThread.SyncProjekte;'}
procedure TLoadSaveThread.SyncProjekte;
var
  i, j: integer;
  blGeloescht, blGefunden: Boolean;
  tmpProjekt: TProjekt;
begin
  SyncStop;
  try
    if FblStop then Exit;
    if FclProjekte.Count = 0 then Exit;

    while clProjekte.Count > 0 do begin
      tmpProjekt := TProjekt(clProjekte.Extract(clProjekte.Items[0]));
      FreeAndNil(tmpProjekt);
    end;
    if FblStop then Exit;
  except end;

  {$Region 'Alle Projekte aus dem Thread durchgehen'}
  try
    for i := 0 to FclProjekte.Count - 1 do begin
      if TLoadSaveItem(FclProjekte.Items[i]).InheritsFrom(TProjekt) then begin
        tmpProjekt := TProjekt.Create(NIL);
        tmpProjekt.Assign(TProjekt(FclProjekte.Items[i]));

        // Wurde das aktuelle Projekt zwischenzeitlich gelöscht?
        blGeloescht := False;
        for j := 0 to FclDelete_Outbox.Count - 1 do begin
          if TLoadSaveItem(FclDelete_Outbox.Items[j]).InheritsFrom(TProjekt) then begin
            if TLoadSaveItem(FclDelete_Outbox.Items[j]).ID = tmpProjekt.ID then begin
              blGeloescht := True;
              Break;
            end;
          end;
        end;

        // Falls gelöscht, nicht in die VCL-Liste übernehmen
        if blGeloescht then begin
          FreeAndNil(tmpProjekt);
        end else begin
          // Falls nicht gelöscht, nachsehen
          // ob dieser Eintrag zwischenzeitlich bearbeitet und gespeichert wurde.
          // Dann diesen Wert bevorzugen, denn das Abspeichern in der Datenbank ist noch nicht beendet.

          j := 0;
          while j < FclSave_Outbox.Count do begin
            if TLoadSaveItem(FclSave_Outbox.Items[j]).InheritsFrom(TProjekt) then begin
              if TLoadSaveItem(FclSave_Outbox.Items[j]).ID = tmpProjekt.ID then begin
                // falls das Objekt aus dem Datenbankthread identisch ist mit dem, was gespeichert werden soll,
                // war das speichern und anschließende Neu-Laden wohl erfolgreich.
                if TProjekt(FclSave_Outbox.Items[j]).Equals(tmpProjekt) then begin
                  FclSave_Outbox.Delete(j);
                end else begin
                  // Abspeichern noch nicht beendet, deswegen Daten aus der Datenbank mit denen aus dem Zwischenspeicher ersetzen
                  tmpProjekt.Assign(FclSave_Outbox.Items[j]);
                  inc(j);
                end;
              end else inc(j);
            end else inc(j);
          end;

          clProjekte.Add(tmpProjekt);

        end;

      end; // inheritsfrom TProjekt
    end; // Alle Projekte
  except end;
  {$EndRegion 'Alle Projekte aus dem Thread durchgehen'}

  {$Region '"Zu speichernde" -also neue- Projekte laden'}
  try
    i := 0;
    while i < FclSave_Outbox.Count do begin

      // Ist das "zu speichernde" Projekt (in Save_Outbox) denn "schon" in clProjekte enthalten?
      blGefunden := False;
      for j := 0 to clProjekte.Count - 1 do begin
        if TLoadSaveItem(clProjekte.Items[j]).ClassType = TLoadSaveItem(FclSave_Outbox.Items[i]).ClassType then begin
          if TLoadSaveItem(clProjekte.Items[j]).ID = TLoadSaveItem(FclSave_Outbox.Items[i]).ID then begin
            blGefunden := True;
          end;
        end;
      end;

      // Falls noch nicht in clProjekte vorhanden
      if not blGefunden then begin
        if FclSave_Outbox.Items[i].InheritsFrom(TProjekt) then begin
          tmpProjekt := TProjekt.Create(NIL);
          tmpProjekt.Assign(TProjekt(FclSave_Outbox.Items[i]));
          clProjekte.Add(tmpProjekt);
        end;
      end;

      inc(i);
    end;
  except end;
  {$EndRegion '"Zu speichernde" -also neue- Projekte laden'}

  {$Region 'Wurde das zu löschende Projekt nicht mehr erneut aus der Datenbank geladen?'}
  try
    i := 0;
    // Liste der zu löschenden Objekte durchgehen
    while i < FclDelete_Outbox.Count do begin

      blGefunden := False;
      // vergleichen mit dem, was eben geladen wurde
      for j := 0 to FclProjekte.Count - 1 do begin
        if TLoadSaveItem(FclProjekte.Items[j]).ClassType = TLoadSaveItem(FclDelete_Outbox.Items[i]).ClassType then begin
          // Falls das zu löschende Objekt immer noch in der Liste gefunden wird, abbrechen
          if TLoadSaveItem(FclProjekte.Items[j]).ID = TLoadSaveItem(FclDelete_Outbox.Items[i]).ID then begin
            blGefunden := True;
            Break;
          end;
        end;
      end;

      // Wenn das zu löschende Objekt nicht mehr aus der Datenbank geladen wird, wurde es erfolgreich gelöscht
      if blGefunden then begin
        inc(i);
      end else begin
        // Nachverfolgung erfolgreich, da der zu löschende Datensatz nicht erneut aus der Datenbank geladen wurde!
        FclDelete_Outbox.Delete(i);
      end;
    end;
  except end;
  {$EndRegion 'Wurde das zu löschende Projekt nicht mehr erneut aus der Datenbank geladen?'}

end;
{$EndRegion}

Union 19. Aug 2013 21:40

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Und Du bist sicher dass Deine Datenbank-Connection bzw. der dort verwendete Treiber Threadsafe ist? Da dann der Fehler nämlich in einer externen dll ausgelöst wird, ergibt sich auch kein ordentlicher Callstack.

berens 19. Aug 2013 23:29

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Über TAdoQuery und TAdoConnection via Ole/Jet auf eine .mdb Datei. Nicht das was eine professionelle Lösung ist, aber das, womit ich arbeiten muss. Außer in dem Thread selbst verwende ich die Ado-Komponenten nirgendwo, falls das irgendwas zur Sache tut...

Perlsau 20. Aug 2013 00:35

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von berens (Beitrag 1225407)
Über TAdoQuery und TAdoConnection via Ole/Jet auf eine .mdb Datei. Nicht das was eine professionelle Lösung ist, aber das, womit ich arbeiten muss. Außer in dem Thread selbst verwende ich die Ado-Komponenten nirgendwo, falls das irgendwas zur Sache tut...

Womöglich liegt es gar nicht vordergründig an deinem Code oder an den Ado-Komponenten, sondern an einer instabilen Verbindung. Wenn du mal Google nach Verbindung+Netzlaufwerk+Switch+stabil fragst, kommst du der Sache vielleicht näher. Zumindest ich hatte ähnliche Probleme beim Einrichten einer Terminverwaltung für drei Arbeitsplätze ohne Datenbank-Server. Die Daten der drei Arbeits-PCs sollten eigentlich immer auf ein via Switch angeschlossenes Netzlaufwerk gespeichert werden. Daß die Verbindung zur Access-DB, die auf dem von allen dreien erreichbaren Netzlaufwerk lag, ständig abriß, zog jedesmal einen Programmabsturz nach sich. Nach dem Austausch des Switch-Controllers war die Verbindung stabil genug, um keine Programm-Abstürze mehr zu verzeichnen. Leider weiß ich heute nicht mehr, von welcher Marke alter und neuer Switch waren ...

Testen könntest du das außer mit Pings, indem du mal probierst, mit eingeschaltetem Cache zu arbeiten: Alle Daten werden erstmal lokal auf dem Rechner gespeichert, danach wird die Verbindung wieder getrennt. Und immer schön Try-Except-Blöcke verwenden, das vermindert die Absturzhäufigkeit im Fehlerfalle :stupid:

jensw_2000 20. Aug 2013 06:47

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Ich werfe auch noch kurz 3 Aspekte in die Runde...

Bei ADO/dbGo muss jeder Thread, der auf die DB zugreift eine eigene TADOConnection haben.
Sonst funktioniert das Connection Pooling nicht.

Jeder Thread der ADO nutzt muss sein eigenes CoInitialize aufrufen.

Je nach Anwendung würde ich abwägen, ob ich COM ggf. als MULTITHREADED initialisiere.
Das läuft oft runder, wenn dein Programm nicht auf OLE, Clipboard oder Shell Dialoge zugreift.

Furtbichler 20. Aug 2013 07:05

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Ursächlich für diese obskuren Fehler ist der Programmierstil. Er verstößt so ziemlich gegen jedes Gebot der robusten Programmierung: Die Routinen sind zu lang, es werden etwaige auftretende Fehler mit leeren Except-Blöcken einfach unter den Tisch gekehrt (Motto: "Habe keine Ahnung, was hier passiert"). Es wird zwar die Möglichkeit eines Abbruchs kommentiert, aber nirgens umgesetzt (
Delphi-Quellcode:
if terminated then exit
)

Im Code sehe ich 'FclProjekte'. Sag nicht, das das eine Listbox ist. Wenn ja => garantierte Fehlerquelle. Verwende *nie niemals nicht* ein VCL-Control als Datenstruktur. die VCL-Controls dienen *nur* zum Darstellen von Daten.
Delphi-Quellcode:
 
if blGefunden then begin
  FreeAndNil(tmpProjekt);
end else begin
  Synchronize(SyncNewDataset); // <--- was wird denn hier synchronisiert? 
  FclProjekte.Add(tmpProjekt);
end;
Weiterhin erzeugst Du erst ein TProjekt, füllst es, suchst dann in der Liste nach, ob es schon da ist, schmeisst es wieder weg, wenn ja usw.

Das Suchkriterium ist die ID. Verwende eine Dictionary / Hashmap, um zu prüfen, ob Du die ID schon geladen hast. Wenn ja, gehe zum nächsten Record. Nur wenn die ID noch nicht geladene wurde, instantiierst du ein TProjekt, füllst die Eigenschaften und übergibst das an dein Formular. Weniger Klimmzüge => weniger Fehlerquellen.

Unterteile deine Methode in einzelne Methoden, die genau eine Aufgabe erledigen. Verwende *nie niemals nicht* leere Except-Blöcke, sondern logge alle Exceptions.

Zum Eingrenzen des Fehlers:

1. Kommentiere alles aus. Läuft es? => Weiter mit 2.
2. Aktiviere nur die ADO-routinen, d.h. instantiiere dein TProjekt, fülle es, schmeiss es weg. Läuft es? Weiter mit 3.
3. Aktiviere die Listenlogik (nur, wenn es keine Listbox ist). D.h. suche in der Liste usw. Läuft es? Weiter mit 4.
4. Aktiviere die Synchronize-Methoden eines nach dem anderen.
5. Tja, Keine Ahnung, wenn du hier gelandet bist.

Blup 20. Aug 2013 08:27

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von Furtbichler (Beitrag 1225419)
Im Code sehe ich 'FclProjekte'. Sag nicht, das das eine Listbox ist. Wenn ja => garantierte Fehlerquelle. Verwende *nie niemals nicht* ein VCL-Control als Datenstruktur. die VCL-Controls dienen *nur* zum Darstellen von Daten.

Der Call Stack lässt dies als Ursache vermuten.
Insbesondere der Zugriff aus einem Thread auf eine VCL-Komponente ist ein sicherer Absturzkandidat.

berens 20. Aug 2013 08:35

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Alle cl* Komponenten bei mit sind TComponentList. Von allen Datentypen aus der Datenbank jeweils eine, die NUR im Thread benutzt wird (Fcl*) und eine, die NUR außerhalb des Threads benutzt wird (cl*). Bei SyncNewDataset beispielsweise werden alle neu hinzugekommenen Einträge über Synchronize von Fcl* an cl* Übergeben.

Im Thread verwende ich keine VCL-Komponenten, ADO wurde mit CoInitialize etc. entsprechend vorbereitet.

Nochmal: Das komplette Programm läuft zuverlässig und einwandfrei. Das einzige was "stört" ist die Exception-Meldung ansich. Deshalb die leeren try..except Blöcke rund um allen Quelltext, den ich für den Bösewicht halte. Trotzdem kommt die Meldung weiterhin.

Wie kann ich denn von der Pointer-Adresse aus dem Fehlerlogbuch auf einen Variablennamen (z.B. FclProjekte) kommen, um zumindestens die schuldige Komponente benennen zu können? Ich sehe zwar, dass irgend eine Liste Probleme macht. Aber welche??

Danke schonmal für die Antworten. Ich hoffe wir finden was.

p80286 20. Aug 2013 10:24

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von berens (Beitrag 1225434)
Nochmal: Das komplette Programm läuft zuverlässig und einwandfrei. Das einzige was "stört" ist die Exception-Meldung ansich. Deshalb die leeren try..except Blöcke rund um allen Quelltext, den ich für den Bösewicht halte. Trotzdem kommt die Meldung weiterhin.

"Mein Auto läuft einwandfrei, nur dieses Lämpchen, was aussieht wie eine Ölkanne, das stört. Wie kann man das abschalten.

Das ist die gleiche Logik!
Aber bitte mach ruhig.

Gruß
K-H

Uwe Raabe 20. Aug 2013 10:27

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zeig doch mal die Deklarationen und Instanzierungen der TComponentList-Instanzen.

berens 20. Aug 2013 10:40

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
In dem Execute vom Thread habe ich ja auch Protokoll-Funktion (wenn auch zunächst mal nur für Debugger), da dort das try..except nicht greift, habe ich die einzelnen Blöcke mit dem blanko try..except umgeben, um zu schauen, ob die Meldung nun verschindet, bzw. dort z.B. ein Sleep(0) gesetzt, um dort mit einem Haltepunkt anzuhalten. Irgendwie muss ich den Fehler ja eingrenzen.

Bei den TComponentListe hatte ich vor den Tests auch welche mit OwnsObjects=True dabei (wegen Garbage-Collection uns Leaks), das ändert allerdings nix an der Problematik.

Delphi-Quellcode:
  TLoadSaveThread = class(TThread)
  private
    FPanicLevel: integer;
    blStop, FblStop: Boolean;
    FslBenutzer: TStringList;

    FclAlleVorgaenge: TComponentList;
    FclBestellDetail: TComponentList;
    FclLebensStationen: TComponentList;

    FclProjekte: TComponentList;
    FclKontakte: TComponentList;
    FclWiederVorlage: TComponentList;
    FclTicket: TComponentList;
    FclMail: TComponentList;
    FclBestellung: TComponentList;
    FclArtikel: TComponentList;

    FclSave_Inbox: TComponentList;
    FclSave_Thread: TComponentList;
    FclSave_Outbox: TComponentList;

    FclDelete_Inbox: TComponentList;
    FclDelete_Thread: TComponentList;
    FclDelete_Outbox: TComponentList;

    FNewDataSet: TLoadSaveItem;
    FProgressText: string;
    procedure SyncSaveAndDelete;
    procedure SyncNewDataset;
    procedure SyncFinished;

    procedure SyncProjekte;
    procedure SyncKontakte;
    procedure SyncWiederVorlage;
    procedure SyncTickets;
    procedure SyncMails;
    procedure SyncBestellung;
    procedure SyncBestellDetails;
    procedure SyncVorgang;
    procedure SyncArtikel;


    procedure SyncStop;
    procedure SyncBenutzer;
    procedure SyncAblaufschritte;
    procedure SyncProgress;

  public
    clBestellDetail: TComponentList;
    clProjekte: TComponentList;
    clKontakte: TComponentList;
    clWiederVorlage: TComponentList;
    clTicket: TComponentList;
    clMail: TComponentList;
    clBestellung: TComponentList;
    clArtikel: TComponentList;

    clAlleVorgaenge: TComponentList;

    blTerminated: Boolean;
    slBenutzer: TStringList;
    ConnectionString: string;

    OnChange: TNotifyEvent;
    OnProgress: TNotifyEvent;
    ProgressText: string;
    PanicLevel: integer;

    ToDo: integer;

    procedure Execute; override;
    procedure Stop;

    procedure Delete(_LoadSaveItem: TLoadSaveItem);
    procedure Save(_LoadSaveItem: TLoadSaveItem);
    procedure SyncAlles;

    constructor Create(_Suspended: Boolean); reintroduce;
    destructor Destroy; override;
  end;


constructor TLoadSaveThread.Create(_Suspended: Boolean);
begin
  inherited;

  try
    OnChange := NIL;
    FblStop := False;

    FclSave_Inbox := TComponentList.Create(False);
    FclSave_Thread := TComponentList.Create(False);
    FclSave_Outbox := TComponentList.Create(False);

    FclDelete_Inbox := TComponentList.Create(False);
    FclDelete_Thread := TComponentList.Create(False);
    FclDelete_Outbox := TComponentList.Create(False);

    FclProjekte := TComponentList.Create(False);
    clProjekte := TComponentList.Create(False);

    FclKontakte := TComponentList.Create(False);
    clKontakte := TComponentList.Create(False);

    FclWiederVorlage := TComponentList.Create(False);
    clWiederVorlage := TComponentList.Create(False);

    FclTicket := TComponentList.Create(False);
    clTicket := TComponentList.Create(False);

    FclMail := TComponentList.Create(False);
    clMail := TComponentList.Create(False);

    FclBestellung := TComponentList.Create(False);
    clBestellung := TComponentList.Create(False);

    FclBestellDetail := TComponentList.Create(False);
    clBestellDetail := TComponentList.Create(False);

    FclArtikel := TComponentList.Create(False);
    clArtikel := TComponentList.Create(False);

    clAlleVorgaenge := TComponentList.Create(False);

    slBenutzer := TStringList.Create;
    ToDo := 0;
    blTerminated := False;
  except end;
end;



procedure TLoadSaveThread.Execute;
var
  db: TADOConnection;
  q: TADOQuery;
  i: integer;
  ...
begin
  q := NIL;
  db := NIL;
  blBenutzerGeladen := False;
  blVorlagenGeladen := False;
  blMailsGeladen := False;
  blArtikelGeladen := False;
  try
    FProgressText := 'Starten ...';
    Synchronize(SyncProgress);

    CoInitialize(NIL);
    FslBenutzer := TStringList.Create;
    FclAlleVorgaenge := TComponentList.Create(True);
    FclLebensStationen := TComponentList.Create(True);

    FProgressText := 'Verbinde Datenbank ...';
    Synchronize(SyncProgress);
    db := TADOConnection.Create(NIL);
    db.LoginPrompt := False;
    db.ConnectionString := Self.ConnectionString;
    db.Connected := True;

    FProgressText := 'Erstelle Abfrage ...';
    Synchronize(SyncProgress);
    q := TADOQuery.Create(NIL);
    q.Connection := db;

Blup 20. Aug 2013 16:01

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Exceptions in einem Thread zu debuggen ist zumindest problematisch.
Als Ursache kommt der Thread schon in Betracht, aber aufgetreten ist die Exception nach EurekaLog im Hauptthread.
Da nützen dies ganzen try..except-Blöcke wenig.

Aus den bisherigen Code-Schnipseln konnte ich keine Fehlerursache erkennen, aber einige Dinge sind schon aufgefallen:

Warum wird "OwnsObjects" bei allen Listen auf False gesetzt?
Irgendwer muss doch für das Freigeben der Objekte zuständig sein.

Es gibt verschiedene Variablen die sowohl private als auch public auftauchen:
Delphi-Quellcode:
private
  FPanicLevel: integer;
  FProgressText: string;
{...}
public
  ProgressText: string;
  PanicLevel: integer;
Da kann schnell mal was verwechselt werden.

"Execute" sollte eigentlich protected sein, nur der Thread selbst darf diese Methode aufrufen.

Delphi-Quellcode:
public
{...}
  blTerminated: Boolean;
  slBenutzer: TStringList;
  ConnectionString: string;
  OnChange: TNotifyEvent;
  OnProgress: TNotifyEvent;
{...}
Da es keine CriticalSection gibt, die den Zugriff auf öffentlich Properties schützt, darf der Zugriff darauf grundsätzlich nur im Hauptthread bzw. über Synchronize erfolgen.

Diese ganzen "public" Listen sollten eigentlich unabhängig vom Thread existieren.
Vieleicht eine unabhängige Klasse die alle Listen verwaltet und global bereitstellt.
Damit würde der Rest des Programms erst einmal von dieser Threadklasse entkoppelt.

Warum gibt es so viele verschiedene interne Listen?
Eigentlich wird doch immer nur eine Liste zur selben Zeit bearbeited.
Die Liste wird aus der Datenbank gelesen.
Dann im Synchronize mit einer öffentlichen Liste verarbeitet.
Welche das ist, ergibt sich aus dem Kontext.
Danach in die Datenbank geschrieben, der Inhalt gelöscht und steht wieder für die nächste Aufgabe zur Verfügung.

Man könnte überlegen für jede Aufgabe eine Klasse zu erstellen.
Die einzelnen Abgleiche unterscheiden sich im wesentlichen nur in:
- Name des Abgleichs
- SQL-Anweisung und Erstellen, Lesen und Speichern der Objekte
- der öffentlichen Liste für den Abgleich
Da lässt sich sicherlich eine Basisklasse bilden, die schon einen wesentlichen Teil einheitlich implementiert.
Alle Aufgaben in einer Liste die der Thread der Reihenfolge nach abarbeitet und schon sieht alles viel aufgeräumter aus.
Das lässt sich auch viel besser einzeln testen und erweitern.

berens 20. Aug 2013 16:45

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Oben schonmal im Nebensatz: OwnsObject wurde zu Testzwecken überall mal auf "False" gesetzt, weil ich einfach schätzungsweise alles ausprobiert habe, den Fehler zu finden.

Alle doppelten Listen und Variablen verfolgen den Zweck, dass ich auf gar keinen Fall mit dem Mainthread auf Variablen/Objekte von diesem Thread zugreife.

Ich habe das Grundkonzept von Threads und "eiserne Regel", dass man vom Main-Thread nicht Objekte von (m)einem Thread zugreift (und andersrum) schon verstanden, aber ich glaube, ich setze das total falsch um.

Aus lauter Angst, versehentlich auf Variablen und Objekte zuzugreifen die gerade de andere Thread verwendet, habe ich immer alle Variablen und Objekte doppelt. Eine nur für den Main-Thread (z.B. clProjekte, public) und eine nur für den Thread (FclProjekte, private). Einzig in Synchronize(SyncBlabla) werden die Objekte von Thread nach Main kopiert und andersherum. Auch hier erstelle ich wieder ausschließlich neue Objekte, die mit Assign die Werte zugewiesen bekommen, vor lauter Sorge, wenn ich ein Objekt, das im Thread erstellt wurde aus einer TComponentList extrahierte (.Extract) und der ComponentList vom Main-Thread hinzufüge, dass dieses Objekt sich nicht 100% garantiert genau so verhält wie eines, das im Main-Thread erstellt wurde (vll. wegen Owner oder sonst irgendwelchen "Objektinternen"-Werten).

Ich werde mir CriticalSection mal genauer anschauen. Dies habe ich bisher noch nie verwendet, da ich davon ausgegangen bin, dass bei dem TThread-Objekt Synchronize() das empfohlende Pendant zu CriticalSection (Threads mit purer Api) ist.

Ich habe auch schon mehrere Thread-Tutorials durchgelesen. Haut mich, wenn ich einfach dabei gepennt habe, aber vielleicht sammeln wird hier gerade nochmal die wichtigsten Fakten, "DOs und DON'Ts".

Ich programmiere mit diesen Grundsätzen im Hinterkopf, bitte ergänzt oder korrigiert mich hier:
(Der Einfachheit halber nenne ich nachfolgend alle Objekte und Variablen etc. nur noch "Variablen".)

1) Mit dem Main-Thread darf man niemals auf Thread-Variablen zugreifen -weder lesend noch schreibend- die jemals im Thread.Execute verwendet werden

2) Im Thread.Execute darf niemals auf externe Variablen zugegriffen werden -weder lesend noch schreibend- (außerhalb des eigenen Objekts)

3) Im Thread.Execute dürfen keine VCL-Komponenten (speziell auch sichtbare Objekte) verwendet werden

4) Im Thread.Execute darf niemals auf Variablen des Thread-Objekts zugegriffen werden, die irgendwann mal von extern gesetzt oder gelesen werden sollen/können (z.B. public Variablen).

5) Im Thread verwendete Komponenten und Units müssen "Thread-Sicher" sein

6) Regel 1, 2, 4 dürfen evtl. mit CriticalSection umgangen werden???

7) Objekte, die im Thread.Execute erstellt wurden (mit oder ohne Owner) und mit Synchronize an das Hauptprogramm übergeben werden, lassen das Universum kollabieren. Dito in die andere Richtung: Im Hauptprogramm erstellte Objekte -speziell welche mit z.B. Owner=frmMain- dürfen auch mit Synchronize nicht an den Thread.Execute übergeben werden (also der direkte Pointer auf das Objekt), selbst wenn Synchronize die Referenz im MainThread auf dieses Objekt löscht), da der Thread -weshalb auch immer- z.B. auf den Owner zugreifen könnte und alles abstürzt.

8) ".Terminate" setzt einfach nur die Variable "Terminated" auf True. Ob der Thread wirklich beendet wurde, erfahre ich nur, wenn ich dafür eine eigene Variable setzte. (z.B. meine blTerminated)

9) Es ist für TThread.Execute kein Unterschied, ob ich eine TComponentList in .Create oder .Execute erstelle.


So, jetzt bekomme ich von Euch bestimmt gleich dieses o.g. Thread-Halb-Wissen um die Ohrengehauen. Lasset den Sturm beginnen. :thumb:

Ne im Ernst, manchmal mach Delphi wirklich viel im Hintergrund und ich weiss nicht so recht, ob ich "das jetzt machen darf oder nicht". Mit dieser Arbeitsintensiven doppelt und dreifach Abspeicherei der Objekte will ich wirklich 100% sicher gehen, dass mein Programm nicht wegen dem Zugriff von Thread und Mainthread auf die selbe Variable abstürzt.

Der schöne Günther 20. Aug 2013 17:10

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Ich habe dieses Thema seit Bestehen auf meiner "Muss ich mir näher anschauen und nicht nur überfliegen"-Liste, bislang ist das nicht geschehen. Deswegen nur eine Reaktion direkt auf den den letzten Beitrag, ohne die ganze Geschichte zu kennen:

Zitat:

1) Mit dem Main-Thread darf man niemals auf Thread-Variablen zugreifen -weder lesend noch schreibend- die jemals im Thread.Execute verwendet werden
Meinst du mit "Thread-Variablen" lokale Variablen in deiner Execute-Prozedur? Oder Felder deiner von TThread abgeleiteten Klasse?

Ich finde du machst es dir unnötig kompliziert. Es gibt zwei eiserne und einfache Regeln:
  1. Zugriff auf die VCL-Dinge darf nur vom Hauptthread aus geschehen.
  2. Wenn sich mehrere Threads für die gleichen Daten interessieren, müssen diese geschützt werden

Das war's. Zur weiteren Erläuterung:

1) Du bist standardmäßig immer im Hauptthread. Ausnahmen sind die Execute-Methode einer TThread-Klasse und beispielsweise die entsprechenden "Behandle Netzwerkpaket"-Handler von Serverkomponenten wie Indy.

2) Schützen bedeutet: Sicherstellen, dass niemand die Daten lesen (oder ändern) kann, während jemand anderes gerade am ändern ist.

Für reine Variablen bieten sich
Delphi-Quellcode:
TMultiReadExclusiveWriteSynchronizer
(was für ein Klassenname) perfekt an. Erstelle dir einen (oder mehrere), und bevor du eine Variable ändern willst, sage "mutex.beginRead();". Wenn du fertig gelesen hast, sag "mutex.endRead();". Ebenso mit dem Schreiben.

Parallel gibt es kritische Abschnitte, die Delphi Klasse
Delphi-Quellcode:
TCriticalSection
kapselt hier eigentlich schon alles was man braucht.

Ich würde vielleicht nicht einmal ein konkretes technisches Tutorial, wie man Threads in Programmiersprache XY hinbekommt, sondern etwas theoretische Grundlagen lesen. Ich habe gerade leider keinen Vorschlag parat, aber vielleicht jemand anderes.

Es tut niemandem weh, wenn du bsp. im VCL-Thread "Objekte benutzt", die in einem anderen erstellt wurden. Niemandem tut es weh, wenn mehrere Threads gleichzeitig die gleiche Methode auf einem einzigen Objekt aufrufen, und dieses Objekt in einem Thread erstellt wurde, der schon längst tot ist. Es sind eigentlich nur diese beiden Regeln.

Uwe Raabe 20. Aug 2013 17:38

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Wenn ich mal auf den Call-Stack zurück kommen darf: die Fehlermeldung impliziert, daß auf die 21116. Komponente in einem Form (vermutlich dem MainForm) zugegriffen wird. Dieser Zugriff erfolgt in DispatchShortCut was allerdings in einer Schleife alle Komponenten iteriert. Sollte also nicht innerhalb dieser Schleife die Liste (aus einem anderen Thread) verändert werden, sollt dieser Fehler gar nicht vorkommen.

Bedenklich finde ich allerdings diese Zahl 21116, die ja nur über die Schleifenvariable oder einen kaputten Stack entstehen kann. Daher erst mal die Frage: Kann es sein, daß eines deiner Forms eine derart hohe Zahl von Komponenten hat? In dem Fall würde ich blind behaupten, du machst da was falsch. Deine Bemerkung "diese mittels Synchronize() an eine Komponente aus dem Hauptformular übergibt" klingt dabei aber verdächtig.

Mir ist auch aufgefallen, daß du das Execute durch unerwartet viele Synchronize-Aufrufe unterbrichst - insbesondere in der while-Schleife. Damit konterkarierst du aber eigentlich den Sinn eines Threads. Das einzige, was du mit dem Thread erreichst, ist, daß deine Anwendung beim
Delphi-Quellcode:
q.Next;
nicht einfriert, wenn Daten nachgeladen werden müssen. Du könntest ja mal versuchen, die benötigten Daten ohne Verwendung eines Threads mit Hilfe eines DataSetProviders in ein ClientDataSet zu laden. Wie lange dauert das?

Auch der Kommentar "damit auch beim Laden abgebrochen werden kann" hinter dem
Delphi-Quellcode:
Synchronize(SyncSaveAndDelete);
zeugt von einem suboptimalen Ansatz: man kann einen Thread auch (und sogar wesentlich effizienter) ohne Aufruf eines Synchronize abbrechen.

Es ist schwer in deinem Code den wirklichen Fehler zu finden, ohne daß du größere Teile des Projekts zur Verfügung stellst. Aber selbst wenn du das tust, muss sich auch noch jemand finden, der dann noch in die Fehlersuche einsteigt. Das könnte den Rahmen einer kostenlosen Hilfe unter Forenteilnehmern eventuell sprengen.

Olli73 20. Aug 2013 19:05

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von Uwe Raabe (Beitrag 1225553)
Wenn ich mal auf den Call-Stack zurück kommen darf: die Fehlermeldung impliziert, daß auf die 21116. Komponente in einem Form (vermutlich dem MainForm) zugegriffen wird. Dieser Zugriff erfolgt in DispatchShortCut was allerdings in einer Schleife alle Komponenten iteriert.

Sieht fast so aus, als ob DispatchShortCut auch über seine erstellten Speicher-Objekte/Komponenten drüber laufen würde.

berens 20. Aug 2013 19:12

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Genau das ist ja das, was mich die ganze Zeit stutzig gemacht hat: Eine Prozedur im Hauptthread löst eine Exception mit Objekten aus dem Thread aus. Vollkommen unsinnig... Wäre da nicht... Das gute alte Copy&Paste, wenn man einfach 1:1 Sachen aus seiner Technik-Demo in das fertige Programm übernimmt:

Beim gezielten Suchen nach dem Hauptformular habe ich festgestellt, dass nicht nur in meiner Thread-Unit, sonder direkt in der Thread.Execute ein verweis auf das VCL-Hauptformular frmMain ist. Und das natürlich noch an bester Stelle:

Delphi-Quellcode:
            tmpBestellung := TBestellung.Create(frmMain);
            with tmpBestellung do begin
              ID := q.FieldByName('ID').AsInteger;
              refProjekt := q.FieldByName('refProjekt').AsInteger;
              dtBestellung := q.FieldByName('dtBestellung').AsDateTime;
              dtAenderung := q.FieldByName('dtAenderung').AsDateTime;

              LastUpdate := CurrentTick;
            end;
Das gehört in einem Thread natürlich auf keinen Fall so (wie gesagt: ich habe es damals halt erst alles naträglich in einen Thread umgeschrieben :( ).

Eine kleine Änderung in:
Delphi-Quellcode:
tmpBestellung := TBestellung.Create(NIL);
Und ... oh Wunder der Technik: aktuell läuft es. :thumb:

Was mich nun wieder speziell an meine "Regeln" oben erinnert: Was zur Hölle schafft Delphi/Windows mit dem "Owner" so krasses? Ich denke mal, dadurch, dass die (Thread-)Objekte in jedem Thread-Durchlauf neu erzeugt und dann wieder komplett gelöscht werden wird Aufgrund diesen Fehlers jedes Mal eine Nachricht an frmMain gesendet wie "Du besitzt nun ein neues Objekt, für dessen .Free du zuständig bist: tmpBestellung". Wobei dieses Objekt dann gleich wieder gelöscht wird, und das Senden dieser Nachricht an frmMain wahrscheinlich gleichermaßen ein Schwerverbrechen ist, wie direkt frmMain.Caption := 'Hallo' im Thread.Execute zu schreiben.

Uwe: Erklär mir doch bitte noch in 1-2 Sätzen, wie ich den Thread ohne Aufruf eines Synchronize abbrechen kann. Suspend pausiert ja nur, und lässt nicht wirklich ein sauberes Ende zu (Komponenten freigeben, AdoVerbindung ordentlich beenden etc. Soll ich z.B. eine Prozedur schreiben, in der ich über CriticalSection einfach z.B. eine Boolean-Variable setzt, und diese im Thread-Verlauf ganz normal auslese ("if not blStop then begin...")?

"Der schöne Günther": Die Felder meiner von TThread abgeleiteten Klasse.
Wenn ich Dich richtig verstehe, dürfen im Prinzip 1000 verschiedene Threads problemlos gleichzeitig tmpBestellung.ID auslesen, wenn diese irgendwie an das Objekt kommen. Was passiert, wenn dieses Objekt eine public-Variable/Objekt von frmMain ist, z.B. frmMain.AktuelleBestellung (nur als Beispiel!)?

Blup: "Execute" sollte eigentlich protected sein. Korrekt. Korrigiert.

Ich hoffe, dass das Projekt jetzt dauerhaft ordentlich läuft. Ich fahre jetzt alles wieder auf den alten Stand (auch OwnsObject und Freigabe der Objekte etc.) Somit war scheinbar alles mal wieder ein Fehler des Schlampigkeit. Trotzdem ein Rüffel an Delphi, irgendwo könnte ja auch stehen, dass z.B. der Fehler beim Zugriff auf die vom frmMain "ge-owned-en" Objekte (wie ist das Wort?), also die Objekte, deren Owner frmMain ist.

Tut mir wie immer Leid, Eure Arbeits- oder Freizeit stark in Anspruch genommen zu haben.
Ich danke vielmals für die geduldigen Antworten!

Anbei nochmal "meine" überarbeiteten Thread-Regeln, mit der Einladung an alle, die sie lesen, mich zu verbessern:
Speziell zu 8: Was passiert, wenn ich ein Object mit Owner = frmMain an den Thread übergebe, und dieser es löschen soll? Muss ja dann zwangläufig mit Synchronize erfolgen, auch wenn ich mit dem Hauptprogramm selber nie wieder schreibend auf das Objekt zugreifen will. Oder?

Darf ich in CriticalSection auf VCL-Objekte zugreifen? Ich muss mich da mal einlesen.

WICHTIGE Frage: Darf ich denn Variable, die garantiert nur vom Thread geschrieben werden (z.B. blTerminated) jederzeit ohne CriticalSelection schreiben, oder muss ich, weil ich mit frmMain im 10ms Takt "if LoadSave.blTerminated" beispielhaft überprüfe, ob der Thread fertig beendet ist unbedingt die CriticalSelection verwenden? frmMain ließt ja nur!?

Code:
1) Mit dem Main-Thread darf man niemals [I]schreibend [/I]auf Thread-Variablen oder Objekte zugreifen, die jemals im Thread.Execute verwendet werden

2) Im Thread.Execute darf niemals [I]schreibend [/I] auf externe Variablen oder Objekte zugegriffen werden

3) Im Thread.Execute dürfen keine VCL-Komponenten verwendet werden. Wenn irgendwo eine (speziell "Visuelle" - also sichtbare) Komponente referenziert wird (z.B. das Hauptformular), ist das zu 100% ein Fehler! Nicht-VCL-Objekte die z.B. unter "public" bei einer VCL-Komponente stehen, dürfen angesprochen werden(?)

4) Im Thread.Execute darf niemals auf Variablen des Thread-Objekts zugegriffen werden, die irgendwann mal von extern gesetzt oder gelesen werden sollen/können (z.B. public Variablen).

5) Im Thread verwendete Komponenten und Units müssen "Thread-Sicher" sein.

6) Regel 1, 2, 4 dürfen mit CriticalSection umgangen werden.

7) Objekte, die im Thread.Execute erstellt wurden (ohne Owner!) dürfen an das Hauptprogramm übergeben werden, sofern Thread.Execute nie wieder schreibend darauf zugreift.

8) In die andere Richtung: Im Hauptprogramm erstellte Objekte -speziell welche mit z.B. Owner=frmMain- dürfen den Thread.Execute übergeben werden, sofern nur gelesen wird.

9) ".Terminate" setzt einfach nur die Variable "Terminated" auf True. Ob der Thread wirklich beendet wurde, erfahre ich nur, wenn ich dafür eine eigene Variable setzte. (z.B. meine blTerminated) Diese Variable kann nur CriticalSelection oder Synchronize im Thread geschrieben werden, und kann/darf problemlos jederzeit vom Hauptprogramm ausgelesen werden. OnTerminate oder "Finished" ist eine bessere Lösung.

10) Es ist für TThread.Execute kein Unterschied, ob ich eine TComponentList in .Create oder .Execute erstelle.

Olli73 20. Aug 2013 19:48

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von berens (Beitrag 1225565)
Was mich nun wieder speziell an meine "Regeln" oben erinnert: Was zur Hölle schafft Delphi/Windows mit dem "Owner" so krasses? Ich denke mal, dadurch, dass die (Thread-)Objekte in jedem Thread-Durchlauf neu erzeugt und dann wieder komplett gelöscht werden wird Aufgrund diesen Fehlers jedes Mal eine Nachricht an frmMain gesendet wie "Du besitzt nun ein neues Objekt, für dessen .Free du zuständig bist: tmpBestellung". Wobei dieses Objekt dann gleich wieder gelöscht wird, und das Senden dieser Nachricht an frmMain wahrscheinlich gleichermaßen ein Schwerverbrechen ist, wie direkt frmMain.Caption := 'Hallo' im Thread.Execute zu schreiben.

Das (alleine) hat hier nicht das Problem ausgelöst, sondern das was Uwe Raabe in #17 geschrieben hat. DispatchShortCut läuft alle Komponenten des Formulars durch, also auch die, denen du versehentlich im Thread frmMain als owner zugewiesen hast. -> Aber huch, wo ist denn die Komponente hin, mitten in meiner Schleife hat doch der böse böse Thread...


Zitat:

Zitat von berens (Beitrag 1225565)
WICHTIGE Frage: Darf ich denn Variable, die garantiert nur vom Thread geschrieben werden (z.B. blTerminated) jederzeit ohne CriticalSelection schreiben, oder muss ich, weil ich mit frmMain im 10ms Takt "if LoadSave.blTerminated" beispielhaft überprüfe, ob der Thread fertig beendet ist unbedingt die CriticalSelection verwenden? frmMain ließt ja nur!?

Bei boolean gibt es (wahrscheinlich) keine Probleme. Aber Strings z.B.(!) werden bei Bedarf umkopiert, wenn das dann mitten im Lesevorgang passiert ist das schlecht. Aber auch ohne Umkopieren, du änderst z.B. gerade im Thread den String 'abcd' auf 'wxyz', dann könnte der Hauptthrad z.B. 'wxcd', und damit rechnest du wahrscheinlich nicht.

Der schöne Günther 20. Aug 2013 19:58

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Wenn ich Dich richtig verstehe, dürfen im Prinzip 1000 verschiedene Threads problemlos gleichzeitig tmpBestellung.ID auslesen, wenn diese irgendwie an das Objekt kommen. Was passiert, wenn dieses Objekt eine public-Variable/Objekt von frmMain ist, z.B. frmMain.AktuelleBestellung (nur als Beispiel!)?
Nur weil es ein Feld einer Form ist, heißt es nicht, dass man es nicht anfassen darf. Mit "VCL-Dinge" meinte ich bsp. Label aktualisieren, Komponenten verstecken. meineForm.verweisAufIrgendetwasAnderes kann man problemlos aus anderen Threads anfassen. Nur nicht vergessen: Wenn sich mehrere Threads für irgenetwas interessieren könnten, muss das entsprechend geschützt werden.

Zitat:

Darf ich in CriticalSection auf VCL-Objekte zugreifen? Ich muss mich da mal einlesen.
Ein kritischer Abschnitt ist keine Magie. Synchronisationshilfen wie KA oder anderes bringt nur etwas, wenn sich alle Teilnehmer auch daran halten. Und die Delphi VCL-Logik wird sich sicher nicht an deinen selbst eingeführten KA halten.

Wenn du wirklich in einem Thread direkt etwas an der VCL-Oberfläche ändern musst, dann ist dazu
Delphi-Quellcode:
TThread.Synchronize(..)
bzw.
Delphi-Quellcode:
TThread.Queue(..)
da.

Zitat:

Darf ich denn Variable, die garantiert nur vom Thread geschrieben werden (z.B. blTerminated) jederzeit ohne CriticalSelection schreiben, oder muss ich, weil ich mit frmMain im 10ms Takt "if LoadSave.blTerminated" beispielhaft überprüfe, ob der Thread fertig beendet ist unbedingt die CriticalSelection verwenden? frmMain ließt ja nur!?
Könnte man glauben, das reicht aber leider nicht. In der Praxis ist die Wahrscheinlichkeit, dass es da knallt zwar extrem gering, aber wie gesagt (es sind eigentlich wirklich nur die zwei Regeln): Sobald sich mehr als ein Thread dafür interessiert: Absichern.
Zitat:

Zitat von Olli73 (Beitrag 1225570)
Bei boolean gibt es (wahrscheinlich) keine Probleme. [...]

Auf SO gab es vor ein paar Tagen genau das Problem, dass eine Integer-Variable in einem Thread hochgezählt wurde und der Hauptthread griff nur lesend zu. Hat auf Dauer auch nicht geklappt. Ich würde nie davon ausgehen, dass da irgendetwas atomar abläuft. Auch bei Boolean nicht.

Außerdem wiederhole ich gerne noch einmal mein "Du machst es dir unnötig kompliziert": Dein TThread-Objekt hat ganz komfortabel schon Eigenschaften wie
Delphi-Quellcode:
Terminated
oder
Delphi-Quellcode:
Finished
. Die kannst du einfach lesen und brauchst dir keine Sorgen machen. Und nichts eigenes erfinden.

berens 20. Aug 2013 20:14

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Günther: Bin mir nicht sicher, aber ich glaube in Delphi 2007 gibt es TThread.Finished nicht.

TThread.Terminate setzt ja nur die Boolean-Variable des Threads auf Terminated, ich kann ja (ohne das Finished von oben) nicht so einfach ohne eigene Variable feststelle, ob .Execute nun ordnungsgemäß beendet wurde?

Klar, es gibt Event.WaitFor. Damit habe ich noch nie gearbeitet, und es scheint doch unverhältnismäßig komplizierter zu sein, als eine einfache Booleanvariable im Thread.

Zumindest das Beispiel aus der OH ist nicht wirklich selbsterklärend:
Delphi-Quellcode:
Event1.ResetEvent(); { Ereignis vor der Ausführung der Threads zurücksetzen }
for i := 1 to Counter do
TaskThread.Create(False); { Aufgaben-Threads erzeugen und ausführen }
if Event1.WaitFor(20000) <> wrSignaled then
raise Exception;
{ nun den Haupt-Threads fortsetzen Alle Aufgaben-Threads sind beendet }
Woher weiß z.B. das Event, dass es auf meinen Thread warten soll? Hier in dem Beispiel werden mehrere Threads erzeugt. Woher weiß das Event, dass alle fertig sind? Wird wrSignaled immer von Thread nach der Beendigung von Terminate gesetzt? Fragen über Fragen.

Weiterhin Danke an Alle! :)

Der schöne Günther 20. Aug 2013 20:30

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Falls es die
Delphi-Quellcode:
Finished
-Property noch nicht gibt, dann doch wenigstens das
Delphi-Quellcode:
onTerminate
-Ereignis? Das wird ausgeführt, wenn der Thread seine Execute-Methode verlassen hat. Als Bonus wird die Methode bereits im Kontext des Hauptthreads ausgeführt, du kannst also schon gefahrlos an VCL-Komponenten herumwerkeln.

Darin kannst du ja dann auch einfach eine
Delphi-Quellcode:
meineForm.derThreadIstFertig := True
setzen.

Uwe Raabe 20. Aug 2013 21:21

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von berens (Beitrag 1225565)
Uwe: Erklär mir doch bitte noch in 1-2 Sätzen, wie ich den Thread ohne Aufruf eines Synchronize abbrechen kann.

Innherhalb des Execute fragst du regelmäßig Terminated ab. Wenn du von außerhalb den Thread beenden willst, rufst du MyThread.Terminate (oder wie die Variable auch heißt, in der du dir Thread-Instanz ablegst) auf. Das ist der empfohlene Weg.

Willst du im Hauptthread auf das Ende von MyThread warten, hilft MyThread.WaitFor (gegebenfalls mit einem Timeout).

Olli73 20. Aug 2013 21:26

AW: Exception ohne wirklichen Auslöser treibt mich in den Wahnsinn!
 
Zitat:

Zitat von berens (Beitrag 1225576)
Zumindest das Beispiel aus der OH ist nicht wirklich selbsterklärend:
Delphi-Quellcode:
Event1.ResetEvent(); { Ereignis vor der Ausführung der Threads zurücksetzen }
for i := 1 to Counter do
TaskThread.Create(False); { Aufgaben-Threads erzeugen und ausführen }
if Event1.WaitFor(20000) <> wrSignaled then
raise Exception;
{ nun den Haupt-Threads fortsetzen Alle Aufgaben-Threads sind beendet }
Hier in dem Beispiel werden mehrere Threads erzeugt. Woher weiß das Event, dass alle fertig sind?

Der Letzte macht das Licht aus ;)
Das steht weiter oben im Beispiel der OH:

Delphi-Quellcode:
 procedure TDataModule.TaskTerminateThread(Sender: TObject);
 begin
   ...
   CounterGuard.Acquire; { Zähler mit einer Sperre belegen }
   Dec(Counter);  { Wert der globalen Zählervariable verringern }
   if Counter = 0 then
     Event1.SetEvent; { Signalisieren, ob es der letzte Thread ist }
   Counter.Release; {Sperre vom Zähler entfernen}
   ...
 end;
Der Counter wird im OnTerminate eines jeden Threads heruntergezählt (mit critical section versteht sich!). Wenn 0 erreicht ist, weiß der Thread, dass er der letzte ist und macht das Lich aus (SetEvent).


Alle Zeitangaben in WEZ +1. Es ist jetzt 15:20 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz