Interrupt bei einem GPIO Pin des RasPi

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: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Socke »

mschnell hat geschrieben:
Di 11. Mai 2021, 13:28
Achtung; Da beliebig viele Events/Mitteilungen in der Queue warten können,. muss man die an den Mainthread zu übertragenden Daten ebenfalls Queuen. Ein hübscher Trick ist dafür eine Transport Klasse zu definieren, jeweils ein Daten-Transport Element zu instanzeren und diese Instanz als Parameter per "Queue" zu übergeben und die Insanz-Variable dann zu vergessen. Im Maintread dann die Daten rausholen und free aufrufen um das Transport-Element zu zerstören.
Ich musste deinen Text mehrmals lesen - und habe ihn dann mal nach Pascal übersetzt.
Man nehme ein Formular mit Listbox und Button.

Code: Alles auswählen

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

  TThreadDataContainer = class(TObject)
  public
    SyncData: String;
    procedure SyncMethod;
  end;

  TMyThread = class(TThread)
  protected
    procedure Execute; override;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    ListBox1: TListBox;
    procedure Button1Click(Sender: TObject);
  private
    mythread: TMyThread;
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Assigned(mythread) then
  begin
    mythread.Terminate;
    mythread.WaitFor;
    mythread.Destroy;
    mythread := nil;       
    ListBox1.AddItem('Thread terminated', nil);
  end
  else
  begin             
    ListBox1.AddItem('Thread started', nil);
    mythread := TMyThread.Create(False);
  end;
end;

{ TThreadDataContainer }

procedure TThreadDataContainer.SyncMethod;
begin
  // Der direkte Zugriff auf Form-Controls von Außen ist keine sauberes Entwurfsmuster
  Form1.ListBox1.AddItem(Self.SyncData, nil);
  Self.Destroy;
end;

{ TMyThread }

procedure TMyThread.Execute;
var
  Container: TThreadDataContainer;
begin
  while not Terminated do
  begin
    Container := TThreadDataContainer.Create;
    // Nur als Beispiel geeignet: FormatDateTime ist nur threadsicher, wenn FormatOptions mitgegeben wird.
    // Da DefaultFormatSettings aber nicht geändert wird, ist das für eine Demo in Ordnung.
    Container.SyncData := FormatDateTime('yyyy-mm-dd hh:nn:ss', now());
    Queue(@Container.SyncMethod);
    // Objektreferenz vergessen; wird im Mainthread aufgerufen
    Container := nil;
    Sleep(1000);
  end;
end;

end.
Die Herausforderung bei TThread.Queue ist, dass man Daten nur im Self-Parameter der angegebenen Methode übergeben kann. Für eine saubere Softwarearchitektur muss man hier eigentlich immer auf ein eigenes Datenobjekt ausweichen. Es gibt zwar noch ein paar andere Schweinerein, aber lesbaren und wartbaren Code hat man damit nicht.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

PascalDragon
Beiträge: 823
Registriert: Mi 3. Jun 2020, 07:18
OS, Lazarus, FPC: L 2.0.8, FPC Trunk, OS Win/Linux
CPU-Target: Aarch64 bis Z80 ;)
Wohnort: München

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von PascalDragon »

Socke hat geschrieben:
Di 11. Mai 2021, 15:15
Die Herausforderung bei TThread.Queue ist, dass man Daten nur im Self-Parameter der angegebenen Methode übergeben kann. Für eine saubere Softwarearchitektur muss man hier eigentlich immer auf ein eigenes Datenobjekt ausweichen. Es gibt zwar noch ein paar andere Schweinerein, aber lesbaren und wartbaren Code hat man damit nicht.
Sobald FPC Unterstützung für Closures hat, wird sich das erledigt haben. :)
FPC Compiler Entwickler

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Nimral »

Ich meine, das Problem der Datenübergabe wurde - in Bezug auf Datenübergabe an den Main-Thread - bereits sehr gut gelöst.

Code: Alles auswählen

TApplication.QueueAsyncCall(const AMethod: TDataEvent; Data: PtrInt)
Diesem Aufruf kann direkt ein Pointer, z.B. für ein Data Objeckt mitgegeben werden, man muss allerdings die AMethod so formulieren dass sie den Pointer als (einzigen) Parameter entgegen nehmen kann. Das ist ein gut verständliches und logisches und einfach zu verwaltendes Konstrukt. Seltsam und schade, dass das für allgemeine Threads nicht geht, nur für den Main-Thread.

Michael ... wie kann man eigentlich TThread.Queue konkret Daten mitgeben? Soweit ich sehe verkraftet es nur einen Methodenzeiger, und keinerlei Struktur für Daten. Du schreibst da "... nur im Self-Parameter der angegebenen Methode übergeben ...". Was genau hast Du gemeint? Wie würde das konkret in Code aussehen?

Armin.

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von mschnell »

PascalDragon hat geschrieben:
Mi 12. Mai 2021, 09:20
Sobald FPC Unterstützung für Closures hat, wird sich das erledigt haben. :)
Ja. Da habe ich vor einigen Jahren im Developers Forum auch diskutiert. Abwer anscheied gibt es das ja immer noch nicht. Deshalb ist diese Methode auch ein "Trick" und nicht 100 % "sauber". Man darf nur das "free" nicht vergessen . Uns es ist natürlich "ungewoht" ein free in einer Methode der Klasse zu machen, also einfach "free;" und nicht "xyz.free;"
Ich denke "Closures" machen genau das ohne dass man "free;" tippen muss.
Sollte aber problemlos funktionieren.
-Michael
Zuletzt geändert von mschnell am Mi 12. Mai 2021, 13:02, insgesamt 2-mal geändert.

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von mschnell »

Nimral hat geschrieben:
Mi 12. Mai 2021, 11:34
"... nur im Self-Parameter der angegebenen Methode übergeben ...".
Die übergebene Methode ist eine Methode der Transport-Klasse und hat dadurch natürlich Zugriff auf die in dieser Klasse definierten Daten.
Ob das "schön" ist, Daten und Funktionalität zu vermengen darüber streiten sich die Puristen seit langem :)
-Michael

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von mschnell »

Nimral hat geschrieben:
Mi 12. Mai 2021, 11:34

Code: Alles auswählen

TApplication.QueueAsyncCall(const AMethod: TDataEvent; Data: PtrInt)
QueueAsyncCall ist tatsächlich mindestens so gut wie TThread.Queue. (ob man nun den Daten- und den Methoden "self" gemeinsam oder getrennt übergeben will ist m.E. Geschmackssache.)
QueueAsyncCall gibt es aber in Delphi nicht. und wenn man auf Delphi-Kompatibilität achten will, sollte man es nicht verwenden.
Historisch gibt es TThread.Queue in Delphi schon sehr lange, wurde aber wenig beachtet und auch erst spät in Lazarus implementiert (auf meinen Wunsch), weil es in der Delphi Dokumentation vergessen worden war.
-Michael

PascalDragon
Beiträge: 823
Registriert: Mi 3. Jun 2020, 07:18
OS, Lazarus, FPC: L 2.0.8, FPC Trunk, OS Win/Linux
CPU-Target: Aarch64 bis Z80 ;)
Wohnort: München

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von PascalDragon »

Nimral hat geschrieben:
Mi 12. Mai 2021, 11:34
Ich meine, das Problem der Datenübergabe wurde - in Bezug auf Datenübergabe an den Main-Thread - bereits sehr gut gelöst.

Code: Alles auswählen

TApplication.QueueAsyncCall(const AMethod: TDataEvent; Data: PtrInt)
Das existiert aber nur in LCL Anwendungen. TThread.Queue ist auch in Anwendungen ohne LCL verfügbar.
mschnell hat geschrieben:
Mi 12. Mai 2021, 12:46
PascalDragon hat geschrieben:
Mi 12. Mai 2021, 09:20
Sobald FPC Unterstützung für Closures hat, wird sich das erledigt haben. :)
Ja. Da habe ich vor einigen Jahren im Developers Forum auch diskutiert. Abwer anscheied gibt es das ja immer noch nicht.
Deswegen habe ich ja geschrieben „sobald”.
FPC Compiler Entwickler

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Nimral »

Michael,

Ich hab jetzt eine Stunde lang drüber nachgedacht, wie ich mit dem was ich bisher weiß konkret mit TThread.Queue Daten übergeben würde, und mit dem was ich bisher weiß nur folgende Lösung gefunden, von der ich annehme dass sie funktionieren könnte:

Code: Alles auswählen

TTransportObject = class
  FValue: String
  property Value: String read FValue write SetValue;
end;

TReceiverThread = class
  FValue: String;
  procedure GetTransportValue(AObject:TTransportObject);
end;

Procedure TReceiverThread.GetTransportValue(AObject:TTransportObject)	
  FValue :=  AObject.Value;
  AObject.Free;
end;
	
Procedure TSenderThread.Execute;

var
   o:TTransportObject;
   
   ...
	o:= TTransportValue.create;
	o.Value := 'Bla';
	TThread.Queue(ReceiverThread,ReceiverThread.GetTransportValue(o))

Dein Hinweis oben klang nicht so, als ob so eine einfache Konstruktion damit gemeint wäre. Was genau hast Du mit dem "Self-Parameter" gemeint?

(oops, ich sah gerade Socke's Post ... da könnte die Lösung drinnen stecken, Moment, muss mich drüber her machen ...)

Armin.

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: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Socke »

Nimral hat geschrieben:
Mi 12. Mai 2021, 14:19
Ich hab jetzt eine Stunde lang drüber nachgedacht, wie ich mit dem was ich bisher weiß konkret mit TThread.Queue Daten übergeben würde, und mit dem was ich bisher weiß nur folgende Lösung gefunden, von der ich annehme dass sie funktionieren könnte:
Da hatte ich mir gestern schon Gedanken zu gemacht ;-) Schau dir mein Beispiel mal an; in deinem Quelltext sind noch ein paar Syntaxfehler enthalten.
Bei TThread.Queue wird die angegebene Methode immer in die Queue des Mainthreads gestellt. Dein ReceiverThread hat keine eigene Queue, in der etwas hineingestellt werden kann. Thread-zu-Thread-Kommunikation musst du erst selbst implementieren.
TThread.Queue kann einen Thread als Parameter entgegen nehmen, wenn man einen anderen Thread als Absender hinterlegen möchte (den Parameter hätte man auch semantisch benennen können ....)
Nimral hat geschrieben:
Mi 12. Mai 2021, 14:19
Dein Hinweis oben klang nicht so, als ob so eine einfache Konstruktion damit gemeint wäre. Was genau hast Du mit dem "Self-Parameter" gemeint?
Wenn du einen Methodenzeiger an TThread.Queue übergibst, enthält dieser Methodenzeiger eigentlich zwei Elemente:
  • Ein Zeiger auf das Objekt, zu dem die Methode gehört
  • Ein Zeiger auf den Startpunkt der Methode
Der Zeiger auf das Objekt wird implizit beim Aufruf der Methode im Parameter "Self" übergeben.
Du kannst diese Elemente auch einzeln bearbeiten:

Code: Alles auswählen

type 
  TMyDataObject = class(TObject)
  public
    procedure SyncMethod;
  end;

// ...

var  
  Data: TMyDataObject;
  MethodPointer: TThreadMethod;
begin
  Data := TMyDataObject.Create;
  MethodPointer := @Data.SyncMethod;
  MethodPointer(); // Methode aufrufen. In SyncMethod verweist "Self" ganz normale auf das Objekt "Data"
  TMethod(MethodPointer).Data := Pointer(5); // Objektreferenz überschreiben
  MethodPointer(); // Methode aufrufen. In SyncMethod enthält "Self" jetzt den Wert "5". Ein Zugriff auf das Objekt "Data" ist nicht möglich.
end;
Geht. Ist aber nicht schön und gehört verboten. Daher sind die Daten als Eigenschaften/Felder von TMyDataObject zu übergeben.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Nimral »

Socke, habs auch gerade bemerkt, dass ich Deinen Hinweis gestern überlesen habe. Ich nage gerade an Deinem Beispiel. Es funktioniert, aber wieso eigentlich?

Wo mein Hirn sofort einen Purzelbaum geschlagen hat ... in execute: es wird der Container erzeugt, dann seine SyncMethod an Queue übergeben (der Aufruf der ja nicht blockt, und daher gehts jetzt unmittelbar weiter ...) und sofort danach wird Container zerstört. Das müsste doch ein SIGSEV sein, sobald SyncMethod im anderen Thread aufgerufen wird.

Das Transfer-Objekt darf doch erst zerstört werden, wenn es sicher angekommen ist, und das weiß eigentlich nur SyncMethod, wenn es aufgerufen wurde?

Armin.

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: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Socke »

Nimral hat geschrieben:
Mi 12. Mai 2021, 14:53
Wo mein Hirn sofort einen Purzelbaum geschlagen hat ... in execute: es wird der Container erzeugt, dann seine SyncMethod an Queue übergeben (der Aufruf der ja nicht blockt, und daher gehts jetzt unmittelbar weiter ...) und sofort danach wird Container zerstört. Das müsste doch ein SIGSEV sein, sobald SyncMethod im anderen Thread aufgerufen wird.
Im Thread wird der Container gar nicht zerstört. Es wird nur die Referenz auf nil gesetzt. Das eigentliche Objekt bleibt im Speicher liegen, der Thread kann aber nicht mehr darauf zugreifen.
Nimral hat geschrieben:
Mi 12. Mai 2021, 14:53
Das Transfer-Objekt darf doch erst zerstört werden, wenn es sicher angekommen ist, und das weiß eigentlich nur SyncMethod, wenn es aufgerufen wurde?
Richtig. Und genau das passiert in TThreadDataContainer.SyncMethod. Diese Methode wird im Haupthread aufgerufen, sobald dieser bei nächster Gelegenheit die Queue abarbeitet.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Nimral »

Ok, ich habs fast geschafft. Zur Kontrolle schnell das Ganze rekapituliert in meinen Worten:

In der Definition von Queue() muss man die Adresse der Routine angeben, da auch gleich noch Parameter hinten dran zu hängen ist nicht möglich. Daher muss die Routine in ein Objekt eingebaut werden, welches man aber dann nicht an Queue() übergeben kann, weil Queue nun mal einen Zeiger auf eine Routine erwartet und kein Objekt.

Dass das Objekt unsichtbar mitwandert, wenn man den Zeiger auf eine Methode des Objekts übergibt, ist "Compiler Magic".

Ebenso "Magic": Queue hängt Aufrufe (was durchaus irgendwo in der Doku steht) ausschließlich in den Main-Thread, das Einhängen von Aufrufen in den Kontext anderer Threads ist nicht möglich. Dass man bei der Form TTread.Queue() einen Thread als 1. Parameter angeben kann hat andere Gründe und macht nicht das was man vermuten würde. Eigentlich macht da aber von innerhalb von Execute eines anderen Threads nur Self Sinn, was dann auf das Selbe hinausläuft wie gleich Self.Queue() zu bemühen.

So weit, so gut, so unintuitiv. Aber da es funktioniert, was solls. Mich stört aber im Moment noch das in der freien Luft herumhängende Transfer-Objekt, das dann per Code fest mit Form1 verdrahtet ist. Ich würde es verständlicher finden, wenn in diesem Fall auch das Transfer-Objekt irgendwie fester Bestandteil von Form1 wäre, so dass man gleich beim Aufruf von Queue sieht was Sache ist. Mal sehen, ob ich eine Lösung dafür finde. Es wird wohl darauf hinauslaufen, das Transport-Objekt nicht selber zu erzeugen, sondern bei Form1 anzufordern. Ich glaube das bekomme ich selber hin.

Nun noch zum letzen Knackpunkt: das Transfer-Objekt wird nach dem Abarbeiten der Transfer-Routine von ihr selbst freigegeben.

Code: Alles auswählen

{ TTransportObject }

procedure TTransportObject.PushTransportedValue;

begin
  Form1.ListBox1.AddItem(Self.Value, nil);
  Self.Destroy;
end;

{ TWorkerThread }

procedure TWorkerThread.Execute;

var
   o:TTransportObject;

begin
  while not Terminated do
  begin
    o:= TTransportObject.create;
    o.Value := FormatDateTime('yyyy-mm-dd hh:nn:ss', now());
    TThread.Queue(Self,@o.PushTransportedValue);
    // Self.Queue(@o.PushTransportedValue);
    o := nil;
    Sleep(1000);
  end;
end;
Wozu genau ist dann der Aufruf von

Code: Alles auswählen

o := nil;
gut? Ich habe ihn mal auskommentiert, es ändert sich augenscheinlich nichts, das Programm arbeitet normal und beendet ohne Memory-Leak. Überhaupt scheint das Zuweisen von NIL an ein Objekt das Objekt nicht freizugeben, so wie es auch in der Doku steht:
" assigning nil to a class or pointer variable will not release (i.e. de-allocate) the memory that is possibly been occupied by the referenced structure."
Wozu ist die Zeile nun gut, wenn nicht dafür das Transfer-Objekt zu zerstören? Und warum fliegt mir das Ganze nicht um die Ohren, wenn ich die Zuweisung rausnehme?

Thnx, Armin.

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Nimral »

Socke hat geschrieben:
Mi 12. Mai 2021, 16:26
Im Thread wird der Container gar nicht zerstört. Es wird nur die Referenz auf nil gesetzt. Das eigentliche Objekt bleibt im Speicher liegen, der Thread kann aber nicht mehr darauf zugreifen.
Ich denke, da habe ich meine Antwort? Es tut nix, ist aber eventuell eine gute Sicherheitsmaßnahme für die Zukunft, wenn in Execute vielleicht noch ein Haufen Code dazu kommen sollte, damit keiner in Versuchung kommt, mit dem Transfer-Objekt noch irgendwas tun zu wollen. Es spiegelt quasi den Zustand, der irgendwann vom anderen Thread herbeigeführt werden wird, in den Kontext des aufrufenden Codes.

Armin.

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: Interrupt bei einem GPIO Pin des RasPi

Beitrag von Socke »

Nimral hat geschrieben:
Mi 12. Mai 2021, 19:23
Ich denke, da habe ich meine Antwort? Es tut nix, ist aber eventuell eine gute Sicherheitsmaßnahme für die Zukunft, wenn in Execute vielleicht noch ein Haufen Code dazu kommen sollte, damit keiner in Versuchung kommt, mit dem Transfer-Objekt noch irgendwas tun zu wollen. Es spiegelt quasi den Zustand, der irgendwann vom anderen Thread herbeigeführt werden wird, in den Kontext des aufrufenden Codes.
Genau. Es ist ein Hinweis an mein zukünftiges Ich oder an einen anderen Entwickler: Ab hier ist die Variable "Container" nicht mehr zu verwenden. Der Aufruf einer Methode von einem Objekt "nil" ist deutlich einfacher als Fehler zu erkennen, als wenn zufällig doch noch ein gültiges Objekt hinterlegt ist.
MfG Socke
Ein Gedicht braucht keinen Reim//Ich pack’ hier trotzdem einen rein

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: Interrupt bei einem GPIO Pin des RasPi

Beitrag von mschnell »

Socke hat geschrieben:
Mi 12. Mai 2021, 14:46
Geht. Ist aber nicht schön und gehört verboten.
Was genau "gehört verboten" (explizit, nicht Beispiel)

P.S.: In modernen C++ - Versionen scheinen die "Smart Pointer" inzwischen sauber und effektiv zu funktionieren. Man braucht eigentlich keine Denstructoren ( Pascal: "myclass.free"; c++: "~myclass()" ) mehr. Dann hat sich das mit den "Closurs" auch erledigt.
Im FPC-Forum ist das vor einigen Jahren intensiv diskutiert worden. Ich weiß nicht, wie das Resultiat jetzt ist.
-Michael

Antworten