Zugriffe auf Datenstruktur synchronisieren

Für Fragen zur Programmiersprache auf welcher Lazarus aufbaut
mintpc
Beiträge: 124
Registriert: Mo 6. Sep 2010, 18:39
OS, Lazarus, FPC: Win 7 (L 1.6 FPC 3.0.0)
CPU-Target: Win 7
Wohnort: Mailand

Zugriffe auf Datenstruktur synchronisieren

Beitrag von mintpc »

Hallo zusammen,

wahrscheinlich schon zig mal gefragt, gefunden hab ich aber nichts. Wenn schon gelöst, reicht mir ein Link.

Mein Problem: Ich erzeuge viele Kreise, die als Liste verwaltet werden:

Code: Alles auswählen

  TKreis = class (TShape)
      public RtgX, RtgY, Groesse : integer;
      public next: TKreis;
      procedure bewegen;
      constructor create (AOwner: TComponent); override;
      procedure setGroessePosition(inX, inY, inGroesse: integer);
      procedure MouseDown (Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
  end;
 
  TListe = class
      erster : TKreis;
      procedure hinzufuegen (inK: TKreis);
      procedure loesche (inK: TKreis);
  end


Screenshot zur Vorstellung.
Screenshot zur Vorstellung.


Wie beim alten "Asteroids" teilen sich die Kreise beim Draufklicken, werden kleiner, die Klone werden in die Liste eingefügt und nach 3 Klicks wird der entsprechende Kreis gelöscht (aus der Datenstruktur entfernt).
Über einen Timer wird die Liste mit einer Schleife durchlaufen, in der die Kreis bewegt werden.

Nun kommt es bei schnellen Klicks und ca. 50-100 Kreisen wohl dazu, dass "gleichzeitig / nebenläufig" ein Kreis gelöscht und die Liste zum Bewegen der Kreise im Timer durchlaufen wird.

Dabei greift die Schleife im Timer wohl auf schon gelöschte Objekte zu und es gibt den SIGSEGV.

Frage: Gibt es die Möglichkeit, die "Nebenläufigkeit" der Prozesse einzuschränken, dass nur gelöscht wird, wenn die Bewegen-Schleife fertig ist?

Vielen Dank schonmal
mintpc

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von wp_xyz »

Du musst eine Statusvariable einführen (FLockList: Integer), die du immer dann, wenn die Liste bearbeitet wird, um 1 erhöhst und, wenn die Bearbeitung beendet ist, wieder um 1 erniedrigst. In allen Routinen, die die Liste bearbeiten möchten, führst du den Aufruf nur dann (oder besser: erst) aus, wenn FLockList = 0 ist.

Code: Alles auswählen

type
  TKreis = class (TShape)
      ...
      procedure MouseDown (Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
  end;
 
  TListe = class
  private
    FLockList: Integer;
  public
    function IsLocked: Boolean;
    procedure Hinzufuegen(inK: TKreis);
    procedure Loesche(ink: TKreis);
  end;
 
procedure TKreis.MouseDown(...);
begin
  inherited;
  repeat
  until not Liste.IsLocked// --> der Kreis muss die Liste kennen!
  Liste.Hinzufuegen(...);
end;
 
function TList.IsLocked: Boolean;
begin
  Result := FLockList <> 0;
end;
 
procedure TList.Loesche(inK: TKreis); // genauso: Hinzufuegen(...)
begin
  inc(FLockList);
  try    // Mit try-finally sicherstellen, dass die routine bei einem Fehler in einem Zustand verlassen wird, in dem die Liste bearbeitet werden kann
    ...
  finally
    dec(FLockList);
  end;
end;

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von braunbär »

Würde ich anders realisieren. Mit einem zusätzlichen Boolean-Feld clicked bei jedem Kreis.

Die Onclick Routine braucht dann nur clicked:=true setzen und kehrt sofort zurück, statt auf die Beendigung einer kompletten "Bewegungsschleife" zu warten. Die Reaktion auf den Click wird dann in der Routine erledigt, die auch für das Bewegen der Kreise zuständig ist, also irgendwas auf die Art

Code: Alles auswählen

 
if self.clicked
then self.KreisSpalten
else self.KreisBewegen
 


Und wenn der Kreis klein genug ist, wird er in der Routine KreisSpalten gelöscht.

BTW: Warum verwendest du nicht die vorgegebene Listenstruktur TLIST statt eines "next" Zeigers im Objekt? Genau für solche Anwendungen ist doch diese Klasse gemacht worden.

@wpxyz
In Delphi würde

Code: Alles auswählen

  repeat
  until not Liste.IsLocked// --> der Kreis muss die Liste kennen!
 

zu einer Endlosschleife führen und das Programm "einfrieren", da müsste es heißen

Code: Alles auswählen

  repeat Application.ProcessMessages
  until not Liste.IsLocked// --> der Kreis muss die Liste kennen!
 

Braucht man in Free Pascal nichts, was dem Delphi Application.ProcessMessages entsprechen würde?

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von wp_xyz »

So genau habe ich mir das nun auch nicht überlegt, der Fragesteller hat zu wenig Information angegeben, um ein konkretes funktionsfähiges Beispiel auszuarbeiten.

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von Warf »

Verwende kein Boolean Feld. Zugriffe auf booleanfelder sind nicht atomar. Genau dafür gibt es critical sections, diese verwenden vom Betriebssystem bereitgestellte atomare get und set Funktionen.

Da ich grade am Handy bin kann ich dir nicht ausführlich erklären warum das notwendig ist, aber wenn die Aktionen nicht atomar sind, kann es sein das trotzdem beide nebenläufigen Prozesse die critical section ausführen.

Es gibt übrigens eine listenklasse die das bereits implementiert, ich glaube tthreadlist oder so

Mathias
Beiträge: 6162
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von Mathias »

Dazu habe ich einen kleine Versuch gemacht.

Code: Alles auswählen

procedure TForm1.FormCreate(Sender: TObject);
var
  i, l: integer;
begin
  l := 100;
  SetLength(Shape, l);
  for i := 0 to l - 1 do begin
    Shape[i] := TShape.Create(Self);
    Shape[i].Parent := Self;
    Shape[i].Left := round(Random(400));
    Shape[i].Top := round(Random(400));
    Shape[i].Shape := stCircle;
    Shape[i].Tag := i;
    Shape[i].OnClick := @ShapeClick;
  end;
end;
 
procedure TForm1.ShapeClick(Sender: TObject);
var
  sh: TShape;
begin
  sh := Shape[TShape(Sender).Tag];
  sh.Visible := False;
end;

Dabei muss beachtet werden, das der Kreis auch verschwindet, wen man nebenan klickt.
Der Grund, die Kreis-Shape, wird beim reinklicken als Quadrat angenommen.

Dies nur so als Tip. :wink:
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

mintpc
Beiträge: 124
Registriert: Mo 6. Sep 2010, 18:39
OS, Lazarus, FPC: Win 7 (L 1.6 FPC 3.0.0)
CPU-Target: Win 7
Wohnort: Mailand

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von mintpc »

Erstmal vielen Dank für die vielen Antworten.

Die Variante von wp_xyz hatte ich so schon ausprobiert, allerdings mit ner booleschen Variablen. Wenn warf Recht hat, dann müsste das ja dann mit integer-Werten klappen.

Braunbärs Lösung klingt auch verführerisch, werde ich auch mal ausprobieren.

Bei Mathias scheint mir, dass der Kreis nur unsichtbar gemacht werden soll, und nicht aus der Liste gelöscht wird, richtig?
Das hatte ich auch schon ausprobiert und das funktioniert auch, wäre aber für mich nur eher so ne Notlösung, falls gar nichts
anderes geht.


Nachher probier ich mal alles aus und berichte dann.

P.S: Das ganze "Programmfragment" hab ich mal angehängt, weil wp_xyz danach gefragt hatte. Der Code ist aber
"schrumpelig", weil ich da grad viel rumprobiert hab. Keine Konstruktoren und so.
Dateianhänge
Asteroids Lazarus.zip
(981.16 KiB) 60-mal heruntergeladen
Zuletzt geändert von mintpc am Mi 30. Aug 2017, 21:49, insgesamt 2-mal geändert.

mintpc
Beiträge: 124
Registriert: Mo 6. Sep 2010, 18:39
OS, Lazarus, FPC: Win 7 (L 1.6 FPC 3.0.0)
CPU-Target: Win 7
Wohnort: Mailand

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von mintpc »

So, getestet.

Die Version von wp_xyz funktioniert leider nicht. Vielleicht ist da von mir ein Umsetzungsfehler drin, aber es kommen recht schnell wieder die Zugriffsfehler. Ich hab die Version angehängt. (Wenn ich was falsch implementiert habe, schonmal vorab Entschuldigung)

Die Version von Braunbär funktioniert. Wildes Mausgeklicke führt zu keinem Fehler mehr. Die Version ist auch angehängt.

Vielen vielen Dank für die großartigen Hilfen. :D
Dateianhänge
Asteroids Lazarus 0.3 Braunbär.zip
(982.06 KiB) 68-mal heruntergeladen
Asteroids Lazarus 0.2 FLockList wp_xyz.zip
(983.74 KiB) 64-mal heruntergeladen

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von Warf »

mintpc hat geschrieben:So, getestet.

Die Version von wp_xyz funktioniert leider nicht. Vielleicht ist da von mir ein Umsetzungsfehler drin, aber es kommen recht schnell wieder die Zugriffsfehler. Ich hab die Version angehängt. (Wenn ich was falsch implementiert habe, schonmal vorab Entschuldigung)

Die Version von Braunbär funktioniert. Wildes Mausgeklicke führt zu keinem Fehler mehr. Die Version ist auch angehängt.

Vielen vielen Dank für die großartigen Hilfen. :D


Dennoch, siehe meinen vorherigen Post, solltest du eine critical section statt einem boolean Feld verwenden. Die Chance das es dir um die Ohren fliegt ist zwar gering, mit critical sections allerdings gar nicht vorhanden.

Wenn dann mach es direkt richtig, und fang nicht mit so einem halbgaren Müll wie booleanfeldern an, bevor du dir noch so einen Fehler angewöhnst

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von braunbär »

Warf hat geschrieben:Verwende kein Boolean Feld. Zugriffe auf booleanfelder sind nicht atomar.

Wie kommst du darauf?
boolean.PNG

Viel atomarer als ein movb oder ein cmpb (s.Anhang) geht nicht. Ob auf der untersten Hardwareebene ein paar zusätzliche Taktzyklen gebraucht werden, um ein einzelnes Byte zu schreiben (beim lesen nicht einmal das), ist für den Programmablauf egal. Da fährt kein Interrupt dazwischen.

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von Warf »

braunbär hat geschrieben:
Warf hat geschrieben:Verwende kein Boolean Feld. Zugriffe auf booleanfelder sind nicht atomar.

Wie kommst du darauf?
boolean.PNG

Viel atomarer als ein movb oder ein cmpb (s.Anhang) geht nicht. Ob auf der untersten Hardwareebene ein paar zusätzliche Taktzyklen gebraucht werden, um ein einzelnes Byte zu schreiben (beim lesen nicht einmal das), ist für den Programmablauf egal. Da fährt kein Interrupt dazwischen.


Der assembler hat nichts zu sagen, critical sections sind per definition atomar, während boolean Felder auf Compiler und Architektur ankommen. Mit der neuen Fpc version könnte das schon ganz anders aussehen. Bei nicht definiertem verhalten darf man niemals Annahmen über das Verhalten machen.

PS: critical sections machen übrigens ein Check und set in einer Aktion, also ein der Wert wird ausgelesen, ein neuer wert wird geschrieben, in einem Tick. Ohne diese atomare Eigenschaft kann zwischen dem get und dem set unterbrochen werden, und dann denken beide Threads die Variable wäre nicht gesetzt und führen gleichzeitig Code aus. Mit critical sections ist das per Definition unmöglich, unabhängig von compilerverhalten, Optimierung oder Architektur

Michl
Beiträge: 2505
Registriert: Di 19. Jun 2012, 12:54

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von Michl »

@Warf: Wenn ich mir die Ausgangsfragestellung ansehe, wird mit Timern in einer Single-Thread-Anwendung gearbeitet. Daher wären hier keinerlei CriticalSections etc. nötig.

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection; 

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von braunbär »

Ich habe mir jetzt dein Programm angeschaut, da sind gibt es zumindest noch zwei Probleme.

1.
Es wäre wirklich gescheiter, das fertige TList zu verwenden, statt mit next-Pointern Eigenbau-Listen zu produzieren, mit denen du dir dann Eigentore schießt. :wink:
Mit Liste.Loesche(index) in timer1timer wird das Listenelement gelöscht und Index auf das nächste Element gesetzt. Gleich darauf kommt noch einmal index:=index.next, d.h. das Element, das hinter deinem gelöschten Element in der Liste sitzt, wird übersprungen. In diesem konkreten Fall wohl nicht sehr schlimm, aber prinzipiell ein unnötiger Fehler. Wenn du eine TList ganz einfach in einer for-Schleife vom letzten zurück zum 1. Element durchläufst (bitte nicht von vorne nach hinten, weil da werden beim Löschen eines Elements die Indizes der noch nicht bearbeitenden Elemente verändert), die Elemente löschst, die gelöscht gehören, und neue Elemente hinten an die Liste anhängst, fällt in deinem Programm ein Gutteil fehlerträchtiger Code weg.

Ich sehe gerade, dass dein Lösch-Code auskommentiert ist und du das Element momentan nur unsichtbar machst. Keine gute Idee. Den Fehler bekommst du aber natürlich erst, wenn du die Kreise richtig löschst, was ja auch gemacht gehört.

2.
Dadurch, dass du Listenelemente in einer asynchronen Timerroutine löschst, kann dir in extrem seltenen Fällen passieren, dass genau dann timer1timer aufgerufen wird, wenn gerade die Mousedown Routine gestartet wurde (also genau zwischen begin und self.clicked:=true) - nicht sehr wahrscheinlich, aber möglich. Wenn dann in der Timerroutine das Element gelöscht wird, auf das du geklickt hast, dann knallt es, weil wenn das Programm von der Timerroutine zurückkommt und self.clicked:=true ausführen will, dann gibt es dieses self nicht mehr.
Nachdem aber bei einem Klick auf ein Element, das in der Zwischenzeit ohnehin schon gelöscht worden ist, nichts mehr zu tun ist, wird es in dem Fall genügen, das self.clicked:=true mit einem Try except zu kapseln, um so die ausgelöste Exception zu ignorieren und einfach weiter zu machen.

braunbär
Beiträge: 369
Registriert: Do 8. Jun 2017, 18:21
OS, Lazarus, FPC: Windows 10 64bit, Lazarus 2.0.10, FPC 3.2.0
CPU-Target: 64Bit
Wohnort: Wien

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von braunbär »

Der assembler hat nichts zu sagen, critical sections sind per definition atomar, während boolean Felder auf Compiler und Architektur ankommen.
Mit der neuen Fpc version könnte das schon ganz anders aussehen. Bei nicht definiertem verhalten darf man niemals Annahmen über das Verhalten machen.

Zum einen würde mich ein Link dazu interessieren.
Zum anderen wäre das in diesem Fall auch egal, weil auf der einen Seite nur gelesen, und auf der anderen nur geschrieben wird.
Entweder das Schreiben passiert "rechtzeitig", dann wird der Click in der aktuellen Schleife verarbeitet, oder es kommt zu spät, dann eben in der nächsten Schleife. Passieren kann nichts. Nebenbei würde ich die Wahrscheinlichkeit, dass in einer zukünftigen FPC Version das atomare movb bzw. cmpb durch irgend etwas anderes ersetzt wird, auf eine glatte Null einschätzen.

Delphi produziert hier übrigens auch ganz atomaren Code. Zumindest Delphi 5 und das RAD Studio 10.1 Berlin. Turbo Pascal 3 ebenfalls. :D
Die Versionen dazwischen habe ich nicht gecheckt.

PS: critical sections machen übrigens ein Check und set in einer Aktion, also ein der Wert wird ausgelesen, ein neuer wert wird geschrieben, in einem Tick. Ohne diese atomare Eigenschaft kann zwischen dem get und dem set unterbrochen werden, und dann denken beide Threads die Variable wäre nicht gesetzt und führen gleichzeitig Code aus. Mit critical sections ist das per Definition unmöglich, unabhängig von compilerverhalten, Optimierung oder Architektur

Das ist schon klar, hat aber mit der Aufgabenstellung hier nichts zu tun.

wp_xyz
Beiträge: 4869
Registriert: Fr 8. Apr 2011, 09:01

Re: Zugriffe auf Datenstruktur synchronisieren

Beitrag von wp_xyz »

mintpc hat geschrieben:Die Version von wp_xyz funktioniert leider nicht. Vielleicht ist da von mir ein Umsetzungsfehler drin, aber es kommen recht schnell wieder die Zugriffsfehler.

Ja, da hast du auch eine recht abenteuerliche Konstruktion hingelegt: Im MouseDown des Kreises sagt dieser der Liste: "Kille mich", was die Liste natürlich sofort erledigt. Aber der Kreis hat noch eine Menge zu erledigen - das MouseDown-Ereignis ist nur der Anfang mehrerer Aktionen, die im Zusammehang mit dem Klick ablaufen. Den Kreis gibt es aber dann schon nicht mehr.

Antworten