fhem Parallelität / Threads

Begonnen von pjakobs, 07 November 2016, 18:11:21

Vorheriges Thema - Nächstes Thema

pjakobs

Ich laufe nun schon zum zweiten Mal in ein Problem:
fhem wirkt zwar an vielen Stellen als sei es eventgesteuert, ist es aber nicht.
Das merkt man erst, wenn man längeren perl Code schreibt (länger im Sinne von läuft länger).

Hier ein Beispiel:
Ich möchte flexibler steuern können, wann und unter welchen Bedingungen ich Geräte abschalte wenn ich das Haus verlasse.

#
# what to do when last person left home
#
sub allAway(){
     my @DevicesToSwitchOffEarly=("LED_Bu", "LED_Sc", "LED_Wo", "LED_Tr", "LED_Ku", "SW_Kaffee", "SW_CDplayer", "ST_Verstaerker");
     my @DevicesToSwitchOffLate=("SW_Aussen");
     my @DevicesToZero=(["SL_Markise","MQ_Markise",0]);
     foreach my $device (@DevicesToSwitchOffEarly){
         # my $device=$_;
         fhem("set $device off");
     }
     foreach my $d (0..@DevicesToZero-1){
         my $setDevice=$DevicesToZero[$d][0];
         my $getDevice=$DevicesToZero[$d][1];
         my $targetValue=$DevicesToZero[$d][2];
         fhem("set $setDevice $targetValue");
         my $maxWait=10;
         my $i =0;
         while((Value($getDevice)!=$targetValue) && ($i <$maxWait)) {
             Log3("allAway:$getDevice",3,"$i...".Value($getDevice));
             sleep(1);
             $i+=1;
         }
     }
     foreach my $device (@DevicesToSwitchOffLate){
         # my $device=$_;
         fhem("set $device off");
     }
}

Drei Listen: Die Geräte, die sofort abgeschaltet werden können, die Geräte die auf null gesetzt werden sollen und die Geräte, die erst danach abgeschaltet werden dürfen.
Hintergrund dazu: meine Markise auf der Terasse lässt sich über ein Kostrukt aus fhem Decvices steuern (ein Slider und ein MQTT Device). Sie hängt gleichzeitig aber an einer über fhem geschalteten Außensteckdose. Wenn ich jetzt blind alles abschalte, dann .. kann ich die Markise halt nicht mehr einfahren.
Die Idee: ich definiere eine Liste von Geräten, die ich auf einen bestimmten Wert bringen muss, bevor ich dann die letzten Geräte abschalten kann.

Nun das Problem: die obige Routine stößt zwar alle fhem Kommandos brav an, aber in der Schleife, wo ich nach Value($getDevice) frage bekomme ich immer nur den Zustand vor dem Einfahren der Markise zurück. Stand sie auf 20%, so bleibt Value(Markise) immer bei 20%, auch im Webfrontend wird der sich ändernde Wert nicht angezeigt.

Das Device ist so definiert:

define MQ_Markise MQTT_DEVICE
attr MQ_Markise IODev MyBroker
attr MQ_Markise room Terasse
attr MQ_Markise stateFormat position
attr MQ_Markise subscribeReading_position /position


Das Problem scheint mir hier zu sein, dass, während eine Funktion ausgeführt wird, das Event-Handling von fhem nicht stattfindet. Was mir fehlt ist eine Funktion wie "yield()" statt "sleep()", die für die Wartezeit die Kontrolle an den Main Task abgeben könnte - hab ich das nur nicht gesehen?

Grüße

pj


pjakobs

wow! das war fix :-)
Schau ich mir an. Sieht aber zumindest in diesem Fall nach einer Möglichkeit aus.
Ein ähnliches Problem haben wir in 32_LedController (https://forum.fhem.de/index.php/topic,55065.0.html)
Möglicherweise lässt sich das auch auf die Art umgehen.

Danke

pj


pjakobs

Merci :-) Im LedController Modul ist mir die Lösung noch nicht ganz klar, da geht's darum, dass der callback aus den HttpUtils nicht abgearbeitet wird, während wir neue Requests erstellen. Mehrere Prozesse würden hier zwar den Knoten zerschlagen, aber nicht unbedingt dafür sorgen, dass die Requests weiterhin in der richtigen Reihenfolge verarbeitet werden würden. Dafür müsse vermutlich das ganze Modul als eigener Task laufen... hmm...

pj

rapster

Hab mir zwar LedController nicht angesehen,
aber sowas ähnliches mache ich in der 74_Unifi mit einer Request-DispatchTable aus denen sich die abgearbeiteten Requests selber austragen, evtl. hilft das ja?
https://sourceforge.net/p/fhem/code/HEAD/tree/trunk/fhem/FHEM/74_Unifi.pm#l382

justme1968

fhem ist event gesteuert. du musst fhem aber auch dir möglichkeit geben events zu erzeugen und zu verarbeiten. mit deiner schleife verhinderst du genau das.

wenn du nach jedem schaltvorgang die kontrolle zurück an fhem gibst und erst dann das nächste device schalest wenn dies möglich ist blockiert nichts.

dazugibt beispiel in structure und LightScene mit asyncDelay oder auch hier:

gruss
  andre
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

pjakobs

Zitat von: justme1968 am 07 November 2016, 19:02:43
fhem ist event gesteuert. du musst fhem aber auch dir möglichkeit geben events zu erzeugen und zu verarbeiten. mit deiner schleife verhinderst du genau das.
na ja, darüber könnte man diskutieren. Sagen wir's mal so: es ist grundsätzlich erstmal single threaded und alle Event Handler hängen im main Thread.
Zitat von: justme1968 am 07 November 2016, 19:02:43
wenn du nach jedem schaltvorgang die kontrolle zurück an fhem gibst und erst dann das nächste device schalest wenn dies möglich ist blockiert nichts.
genau, das ist mir schon klar. Wie gesagt: ein yield() wäre hier eine wunderbare Erweiterung. Damit könnte ich - ggf. für eine definierte Mindestzeit - die Kontrolle an den Event Handler zurückgeben.
Zitat von: justme1968 am 07 November 2016, 19:02:43
dazugibt beispiel in structure und LightScene mit asyncDelay oder auch hier:
Sollte hier ein Link rein?
Zitat von: justme1968 am 07 November 2016, 19:02:43
gruss
  andre
danke,

pj

justme1968

Zitatna ja, darüber könnte man diskutieren.
nein. das ist eine tatsache. aber du schmeisst threads und events durcheinander. fhem ist single threaded und eventgesteuert.

statt yield gibt es das fhem sleep mit dem du einen wieder aufruf deiner routine einplanen kannst. auch mit z.b 0.1 sekunde.

sorry. dieser link: https://forum.fhem.de/index.php/topic,51906.msg436451.html#msg436451 sollte da noch hin. da gibt es ein beispiel dafür.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

pjakobs

ich hab jetzt mal folgendes gebaut:

#
# what to do when last person left home
#
sub allAway(){
     my $hash=$defs{"DU_Dummy"};
     Log3($hash->{NAME},2,"calling BlockingCall");
     $hash->{helper}{RUNNING_PID} = BlockingCall("doAllAway","","doAllAwayDone",30);
}

sub doAllAway(){
     my @DevicesToSwitchOffEarly=("LED_Bu", "LED_Sc", "LED_Wo", "LED_Tr", "LED_Ku", "SW_Kaffee", "SW_CDplayer", "ST_Verstaerker");
     my @DevicesToSwitchOffLate=("SW_Aussen");
     my @DevicesToZero=(["SL_Markise","MQ_Markise",0]);
     foreach my $device (@DevicesToSwitchOffEarly){
         Log3("allAway()",2,"switching off $device");
         fhem("set $device off");
     }
     foreach my $d (0..@DevicesToZero-1){
         my $setDevice=$DevicesToZero[$d][0];
         my $getDevice=$DevicesToZero[$d][1];
         my $targetValue=$DevicesToZero[$d][2];
         fhem("set $setDevice $targetValue");
         my $maxWait=10;
         my $i =0;
         while((Value($getDevice)!=$targetValue) && ($i <$maxWait)) {
             Log3("allAway:$getDevice",3,"$i...".Value($getDevice));
             sleep(1);
             $i+=1;
         }
     }
     foreach my $device (@DevicesToSwitchOffLate){
         # my $device=$_;
         fhem("set $device off");
     }
}

sub doAllAwayDone(){
     my $hash=$defs{"DU_Dummy"};
     Log3($hash->{NAME},2,"finished blocking Routine");
     delete($hash->{helper}{RUNNING_PID});
}



Da der Code ja nicht mit einem Gerät assoziiert ist und mir nicht klar war, wo ich so mein $hash herbekommen könnte habe ich ein Dummy (DU_Dummy) angelegt, an den ich mich hänge.

ein paar Dinge sind merkwürdig:

1. wird zwar der interne Zustand der Geräte geschaltet, das führt aber nicht zu einer externen Aktion. Die SW_.* Devices sind z.B. IT Aktoren, der Zustand ist nach dem Aufruf {allAway()} auf "off", aber über den CUL wurde offenbar nichts versandt. Änlich für meine LED_.* Devices. Die verwenden mein 32_LedControlelr Modul, ich sehe im Log, dass das Modul aufgerufen wird, und dass es selbst bis zum HttpUtils_NonblockingGet() läuft, aber offenbar wird auch hier nichts versandt.
2. wird doAllAwayDone() nie aufgerufen
4. ist Value($getDevice) immer noch konstant der Startwert
3. hängt fhem nach dieser ganzen Aktion und ich muss den perl Prozess killen.

Die Routine doAllAway() kann ja nur über den BlockingCall() aufgerufen worden sein, dennoch sieht es weiterhin so aus, als ob der Code den Rest von fhem blockiert.
Hilft mir jemand, meinen Denkfehler zu lokalisieren?

danke

pj

herrmannj

Hallo pjakobs,

das ist korrekt.

Vielleicht etwas Kontext: threads sind ja, egal in welcher Ausprägung, Prozesse die nebeneinander und *unabhängig* laufen. (Etwas vereinfacht): sie können sich deshalb nicht gegenseitig blockieren weil sie gegenseitig keinen Zugriff auf einander haben.

BlockingCall benutzt die Spielart des fork (http://perldoc.perl.org/functions/fork.html). Beim fork wird mit Hilfe ganz spezieller, speicher-schonender, Techniken ein kompletter "Klon" von fhem erstellt und der "Inhalt" des BlockingCall läuft im Klon. Aber eben nur einmal der Inhalt. Wenn das fertig ist meldet BlockingCall, wieder mit spezieller Technik, über den Callback das Ergebnis an das "richtige" fhem zurück.

Wenn Du jetzt innerhalb der Anweisungen im BlockingCall auf fhem zugreifst, etwa indem Du readings setzt, dann greifst Du in Wirklichkeit auf den Klon zu und setzt dort das reading. Der gesamte Klon wird jedoch nach dem Ende des BlockingCall in das digitalte Nirvana geschickt.

Das "richtige" fhem bekommt also von den Änderungen die Du innerhalb des BlockingCall machst nie etwas mit. Im Gegenzug bremst natürlich alles was Du im BlockingCall machst das richtige fhem auch nie aus (Du kannst testweise sleep dort einbauen.)

Während der BlockingCall läuft kannst Du das überprüfen wenn Du auf der OS-Shell "ps aux | grep perl" eingibst. Dort siehst Du fhem dann zwei mal.

Um das von Dir beschriebene Problem zu lösen ist der Ansatz BlockingCall ungeeignet. Es gibt auch leider keine Antwort in einem Satz.

Grundsätzlich ist es leider so das der Aufwand den man betreiben muss um bestimmte Dinge non-blocking zu erledigen um Zehnerpotenzen größer sein kann. Ein (interaktiver) Systemaufruf Blocking ist ein Befehl. Non-blocking kommen da schnell viele dutzend Zeilen (plus Gehirnschmalz) zusammen.

Für einige Sachen existieren frameworks. BlockingCall eignet sich zum Beispiel sehr gut um einen (langsamen) Systemaufruf durchzuführen und am Ende *ein gesammeltes* Ergebnis an fhem zurückzugeben.

In Deinem Fall würde ich Dir empfehlen folgendes Vorgehen zu untersuchen (und zu entwickeln):

1.Alle Kommandos die Du ausführen möchtest in eine Queue schreiben.
2.Das erste Kommando rausholen (FIFO), ausführen und danach einen Timer mit 0 (oder der minimal möglichen zeit) setzen.
3.Im Callback des Timer  Schritt 2 wiederholen, solange bis die Queue abgearbeitet ist.

Durch den Umweg über den Timer wird fhem in die Lage gesetzt die main loop nach jedem Kommande anzusteuern und dort auf alle anderen Ereignisse zu reagieren.

Unter Umständen geht das in die Richtung die rapster in unify umsetzt. Das bitte prüfen, wenn ja kannst Du Dich daran orientieren.

vg
joerg


pjakobs

Zitat von: herrmannj am 08 November 2016, 11:58:06
1.Alle Kommandos die Du ausführen möchtest in eine Queue schreiben.
2.Das erste Kommando rausholen (FIFO), ausführen und danach einen Timer mit 0 (oder der minimal möglichen zeit) setzen.
3.Im Callback des Timer  Schritt 2 wiederholen, solange bis die Queue abgearbeitet ist.

Durch den Umweg über den Timer wird fhem in die Lage gesetzt die main loop nach jedem Kommande anzusteuern und dort auf alle anderen Ereignisse zu reagieren.
vg
joerg

moin Joerg,

dann sind wir ja wieder an dem Punkt, den wir im LedController schon haben ;-)

Vielleicht wäre es sinnvoll, generell ein "queued command device" anzubieten?
Wobei das in diesem Fall in erster Näherung ja auch nicht hilft, da die DevicesToSwitchOffLate ja eben explizit erst nach dem ganzen Rest geschaltet werden dürfen. Da wäre dann also auch noch ein globaler Zähler gefragt.

Die Queue haben wir ja aber schon, es ist die zentrale "Event Queue" (wobei ich mich immer noch wehre, das ganze als eventgesteuert zu bezeichnen, denn Events sind asynchron, fhem arbeitet im Kern synchron eine Task Queue ab) - In Anbetracht, dass mittlerweile sogar die RaspberryPi mehrere CPU Cores haben wäre es für eine spätere fhem Version wirklich schön, wenn mehrere Threads parallel laufen könnten. Aber das fürchte ich ist mit perl code nicht oder kaum möglich.
Zitat
The "interpreter-based threads" provided by Perl are not the fast, lightweight system for multitasking that one might expect or hope for. Threads are implemented in a way that make them easy to misuse. Few people know how to use them correctly or will be able to provide help.

pj

herrmannj

#12
genau, nur gibt es eben nicht die "all-in-one" Lösung sondern welcher Weg der geeignete ist hängt durchaus von der konkret zu lösenden Aufgabe ab.

Der Hinweis aus der perl Doku zu threads ist *sehr* ernst zu nehmen. Threads sind aus vielen Gründen auch *kein* Allheilmittel. Threads müssen sehr genau synchronisiert werden sonst entstehen deadlocks oder race conditions.

Bleiben wir bei dem Fall das es darum geht mehrere Device "am Stück" zu schalten. Nehmen wir an Du legst das alles in verschiedene parallel laufende threads. Was ist mit dem Ausgabemedium, vielleicht ein HMLAN oder ein CUL? Der kann ja trotz allem nur ein Kommando am Stück absetzen und benötigt dafür Zeit. Die mag im Einzelfall gering sein (wobei es je nach Device durchaus um 1?? ms gehen kann).

Wenn jetzt alle threads gleichzeitig dem Ausgabe Device Kommandos geben entsteht mindestens Chaos. Hier benötigt man also weiter Instanzen die das ordnen und dann doch wieder (wartend) in eine Reihenfolge bringen. Damit steigt die Komplexität exponentiell an und schneller geht es trotzdem nicht. (Kann es nicht).

Die Art und Weise wie fhem das aktuell handhabt (kooperatives Multitasking) ist schon in Ordnung. Die Verantwortung liegt eben nicht beim System (fhem) sondern beim Modulentwickler der aufgerufen ist *jeden* blockierenden Job (das sind in aller Regel EA Aufgaben) strikt zu unterlassen.

vg
joerg


justme1968

innerhalb eines BlockingCall kann man mehrfach BlockingInformParent aufrufen und auch mehrfach daten an den parent melden. um z.b. readings kontinuierlich zu erzeugen.

schau dir mal SubProcess.pm an. dort kommunizieren parent und child bidirektional über sockets. hier muss man aber dann aufpassen die sockets auch non blocking zu machen damit die queues nicht voll laufen und deshalb blockieren.

aber wenn es nur darum geht mehrere devices nacheinander zu schalten ist das alles viel zu übertrieben. scheu dir an wie structure und LightScene das mit async_delay machen.
hue, tradfri, alexa-fhem, homebridge-fhem, LightScene, readingsGroup, ...

https://github.com/sponsors/justme-1968

pjakobs

#14
Um das mal von einer anderen Seite zu betrachten:

Es geht hier ja nicht primär darum, langlaufende Calls zu isolieren (obwohl das natürlich auch toll wäre), sondern darum, dass ein Call (bzw. Set) sofort zurück kommt, auch wenn die dadurch ausgelöste Aktion noch nicht abgeschlossen ist (in diesem Fall: das Einfahren der Markise). Aber: ich muss auf den Abschluss der Aktion warten, bevor ich zur nächsten Gruppe Calls weiter gehe. Innerhalb einer Gruppe können - zumindest momentan - alle parallel laufen.

denkbar wäre dann doch der folgende Ablauf:

SwitchOffEarly();
SetToZero();
sleep Schleife, deren Abbruchbedingung aus den Bedingungen der langlaufenden Vorgänge besteht
SwitchOffLate();

ich stell mir das dann so vor:

wait(@bedingungen){
  my $state=true;
  foreach @bedingung (@bedingungen){
    my ($getDevice, $targetValue) = @Bedingung;
    $state=$state && (Value($getDevice) == $targetValue)
  }
  if (!$state){
    fhem("sleep 1; {wait(@bedingungen)}");
  }
  SwitchOffLate();
}


Was meint Ihr?

noch eleganter wäre es, wenn ich dynamisch ein notify erstellen könnte, dass dann auslöst, wenn die Bedingugnen eintreten, aber ... geht das? Grundsätzlich kann ich doch
fhem("define mynotify notify ");
ausführen und es später wieder löschen, allerdings frage ich mich: was sind die Seiteneffekte?
Hmm... zumindest mit Timern funktioniert das wohl https://forum.fhem.de/index.php/topic,36504.0.html

pj