Funktionswarteschlange oder auch kooperatives Multitasking

Begonnen von Thorsten Pferdekaemper, 11 Oktober 2017, 22:15:10

Vorheriges Thema - Nächstes Thema

Thorsten Pferdekaemper

Hi,
gibt es in FHEM schon so etwas wie eine "allgemeine Funktionswarteschlange"? D.h. ich habe mehrere Funktionen, die ich nacheinander ausführen will, aber nicht so, dass FHEM die ganze Zeit blockiert. Also in etwa eine allgemeine Implementierung davon:

sub f1() {
   blablabla
   InternalTimer(...,f2,...)
}

sub f2() {
   blubbblubbblubb
   InternalTimer(...f3,....)
}

BlockingCall hilft mir nicht viel, da die einzelnen Funktionen Devices anlegen bzw. ändern.

Schön wäre es, wenn man mehrere solcher Warteschlangen anlegen könnte, die dann mit unterschiedlichen Prioritäten quasi-parallel ablaufen. ...am besten noch mit Integration in die fhem.pl-Hauptschleife, so dass normale abgelaufene Timer (also die, die keine Warteschlangen zu benutzen), immer Vorrang haben.

Im Prinzip wäre das dann fast schon so etwas wie kooperatives Multitasking. Wenn es dazu schon etwas gibt, dann wäre das auch interessant.

Gruß,
    Thorsten
FUIP

rudolfkoenig

Zitatgibt es in FHEM schon so etwas wie eine "allgemeine Funktionswarteschlange"
Noch nicht. Andererseits bin ich noch nicht ueberzeugt, dass im FHEM-Umfeld "kooperatives Multitasking" ein gute idee ist.

ZitatBlockingCall hilft mir nicht viel, da die einzelnen Funktionen Devices anlegen bzw. ändern.
Wenn etwas lange dauert, dann kann man die Berechnung in einem BlockingCall auslagern, und die Ergebnisse dem Parent mitteilen, der dann die Aenderungen an den Datenstrukturen durchfuehrt. Calendar macht es mW auch so.

Thorsten Pferdekaemper

Zitat von: rudolfkoenig am 12 Oktober 2017, 10:31:04
Noch nicht. Andererseits bin ich noch nicht ueberzeugt, dass im FHEM-Umfeld "kooperatives Multitasking" ein gute idee ist.
Meiner Meinung nach machen wir den problematischen Teil davon ja schon. FHEM vertraut darauf, dass die Module alles, was vielleicht etwas länger dauert, irgendwie auslagert (BlockingCall) oder in kleinere Häppchen aufteilt und dann per InternalTimer oder so quasi-nebenläufig abhandelt. Letzteres hätte ich gerne vereinfacht. Es müssen ja nicht wirklich Threads mit yield() etc. sein. Deshalb kam ich mit der "Funktionswarteschlange".

Zitat
Wenn etwas lange dauert, dann kann man die Berechnung in einem BlockingCall auslagern, und die Ergebnisse dem Parent mitteilen, der dann die Aenderungen an den Datenstrukturen durchfuehrt. Calendar macht es mW auch so.
Mein Problem ist, dass es gerade die Änderungen an den Datenstrukturen sind, die mir Probleme bereiten. Z.B. werden manchmal Geräte mit 26 Kanälen (oder sogar virtuelle Geräte mit 50 Kanälen) angelegt. Es gibt da nicht ganz sooo viel zu berechnen, aber das "saubere" CommandDefine und das setzen von Default-Attributen kostet Zeit.
Außerdem mag ich nicht, dass BlockingCall tatsächlich parallel läuft und damit das System belastet. Ich hätte gerne etwas, was sich sauber in die fhem.pl-Hauptschleife einreiht und nicht das System langsamer macht.
Natürlich könnte ich das alles mal wieder auch anders lösen, aber so eine Queue wäre praktisch.

Gruß,
   Thorsten


FUIP

CoolTux

Zitat von: rudolfkoenig am 12 Oktober 2017, 10:31:04
Noch nicht. Andererseits bin ich noch nicht ueberzeugt, dass im FHEM-Umfeld "kooperatives Multitasking" ein gute idee ist.
Wenn etwas lange dauert, dann kann man die Berechnung in einem BlockingCall auslagern, und die Ergebnisse dem Parent mitteilen, der dann die Aenderungen an den Datenstrukturen durchfuehrt. Calendar macht es mW auch so.

Verwendet Calendar nicht seit neustem SubProcesses? Hatte doch erst letzte Woche deswegen im Modul gestöbert.



Grüße
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

rudolfkoenig

ZitatFHEM vertraut darauf, dass die Module alles, was vielleicht etwas länger dauert, irgendwie auslagert (BlockingCall) oder in kleinere Häppchen aufteilt und dann per InternalTimer oder so quasi-nebenläufig abhandelt.
Da gebe ich dir recht.

Meinst du wirklich, dass:
sub f1() {
   blablabla
   InternalTimer(...,f2,...)
}

sub f2() {
   blubbblubbblubb
   InternalTimer(...f3,....)
}

schlechter ist, als
sub f1() {
   blablabla
}

sub f2() {
   blubbblubbblubb
}
addToList(f1, param1);
addToList(f2, param2);
...

Und dabei muss man wissen, dass nach einem Select-Pruefung und Behandlung genau ein Element von dieser Liste abgearbeitet wird. Ich habe kein Problem sowas einzubauen (ist relativ trivial), wenn noch jemanden gibt, der sowas gerne haette.

ZitatVerwendet Calendar nicht seit neustem SubProcesses?
Oder so.

CoolTux

Lustiger Weise hätte ich aktuell sogar Bedarf dafür. Ansonsten mache ich es wieder mit InternalTimer



Grüße
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

Thorsten Pferdekaemper

Zitat von: rudolfkoenig am 12 Oktober 2017, 13:14:18
Meinst du wirklich, dass:
sub f1() {
   blablabla
   InternalTimer(...,f2,...)
}

sub f2() {
   blubbblubbblubb
   InternalTimer(...f3,....)
}

schlechter ist, als
sub f1() {
   blablabla
}

sub f2() {
   blubbblubbblubb
}
addToList(f1, param1);
addToList(f2, param2);
...

Ja, das meine ich.
Die einzelnen Funktionen werden z.T. in unterschiedlichem Kontext verwendet. D.h. ich müsste mir dann je nach Verkettung eigene Funktionen bauen. Z.B. kann es passieren, dass 3 Funktionen f1,f2,f3 mal direkt ausgeführt werden sollen, dann soll mal f1,f2,f3 als Sequenz (über die Queue) laufen und mal f1,f3,f1,f2,f1.   
Ich hätte auch gerne Prioritäten. Also z.B.: Ich würde gerne 50 mal f1 aufrufen, aber mit niedriger Priorität. D.h. "wenn FHEM gerade Zeit hat". Währenddessen sollen aber eingehende Messages (also über die Hardware) immer noch zu schnellen Updates der Readings führen. (Das mache ich auch asynchron, da es sonst immer Probleme gab.) So etwas ist mit verallgemeinertem Coding einfacher zu lösen als immer wieder neu.
 
ZitatUnd dabei muss man wissen, dass nach einem Select-Pruefung und Behandlung genau ein Element von dieser Liste abgearbeitet wird.
Genau so hätte ich das auch gerne. Mit InternalTimer werden auf einen Rutsch alle abgearbeitet, die gerade "dran" sind. D.h. wenn ich alles "dumm" gleichzeitig mit InternalTimer abschicke, dann blockiert das auch wieder. (Man sieht es im apptimer nur nicht mehr so gut.)   
Außerdem habe ich den Eindruck, dass die Sortierung bei der Abarbeitung von $intAt nicht unbedingt stabil ist. Ich bin mir nicht sicher, aber es scheint so ein bisschen Zufall zu sein, welche der Funktionen zuerst abgearbeitet wird, wenn die Timestamps gleich sind.

Zitat
Ich habe kein Problem sowas einzubauen (ist relativ trivial), wenn noch jemanden gibt, der sowas gerne haette.
Danke, aber ich wollte gerade keinen Aufwand verursachen. Ich würde gerne selbst daran arbeiten. Ich kann das ja dann hier wieder vorstellen und dann können wir ja sehen, inwiefern das standard-tauglich ist.

Danke&Gruß,
   Thorsten
FUIP

viegener

Ich denke den Bedarf gibt es schon, ich habe das inzwischen in mehreren Modulen, die per httpUtils mit externen Devices kommunizieren. Auch der TelegramBot benutzt das, damit Anfragen hintereinander abgearbeitet werden. Für manche Fälle ist es nötig, da unter einer Authentifizierung nur ein request zu einer Zeit erlaubt ist. Ich denke eskann aber auch helfen die Systemlast zu begrenzen.


Kein Support über PM - Anfragen gerne im Forum - Damit auch andere profitieren und helfen können

Thorsten Pferdekaemper

Hi,
ich habe jetzt in fhem.pl folgendes eingebaut:

#PFE PrioQueue BEGIN
# function: A function reference
# arguments: An array reference
# priority: a number like in unix "nice". Smaller numbers mean higher priority.
#           Is limited to [-20,19], default 0
sub PrioQueue_add($$;$) {
    my ($function, $arguments, $niceness) = @_;
if(not defined($niceness)) {
    $niceness = 0;
}elsif($niceness < -20){
$niceness = -20;
}elsif($niceness > 19) {
$niceness = 19;
};
# empty array in case nothing yet for this niceness
$DB::single = 1;
$prioQueues{$niceness} = [] unless defined $prioQueues{$niceness};
# add entry
push(@{$prioQueues{$niceness}},{"function" => $function, "arguments" => $arguments});
# set next HandleTimeout to "immediate"
$nextat = 1;
};
#PFE PrioQueue END


#####################################
# Return the time to the next event (or undef if there is none)
# and call each function which was scheduled for this time
sub
HandleTimeout()
{
  return undef if(!$nextat);

  my $now = gettimeofday();
  if($now < $nextat) {
    $selectTimestamp = $now;
    return ($nextat-$now);
  }

  $now += 0.01;# need to cover min delay at least
  $nextat = 0;
  #############
  # Check the internal list.
  foreach my $i (sort { $intAt{$a}{TRIGGERTIME} <=>
                        $intAt{$b}{TRIGGERTIME} } keys %intAt) {
    $i = "" if(!defined($i)); # Forum #40598
    next if(!$intAt{$i}); # deleted in the loop
    my $tim = $intAt{$i}{TRIGGERTIME};
    my $fn = $intAt{$i}{FN};
    if(!defined($tim) || !defined($fn)) {
      delete($intAt{$i});
      next;
    } elsif($tim <= $now) {
      no strict "refs";
      &{$fn}($intAt{$i}{ARG});
      use strict "refs";
      delete($intAt{$i});
    } else {
      $nextat = $tim if(!$nextat || $nextat > $tim);
    }
  }

#PFE PrioQueue BEGIN 
  # Only if there is a queued function
  # It would be better to give the entries in intAt higher niceness
  # however, not all modules are "nice", so we do not check this
  if(%prioQueues) {
    $DB::single = 1;
    # get smallest niceness (i.e. highest priority)
my $niceness = min(keys %prioQueues);
# get next function/argument
my $toCall = shift(@{$prioQueues{$niceness}});
# remove whole list if empty
delete $prioQueues{$niceness} unless @{$prioQueues{$niceness}};
# call... this leaves "strict refs" on, so only function refs can be used
&{$toCall->{function}}(@{$toCall->{arguments}});
# if there are still entries in the prioQueue, go on as fast as possible
$nextat = 1 if(%prioQueues);
  };
#PFE PrioQueue END
 
  if(!$nextat) {
    $selectTimestamp = $now;
    return undef;
  }

  $now = gettimeofday(); # if some callbacks took longer
  $selectTimestamp = $now;

  return ($now+ 0.01 < $nextat) ? ($nextat-$now) : 0.01;
}

D.h. mit PrioQueue_add wird ein Eintrag hinzugefügt, der dann in HandeTimeout abgearbeitet wird.
Ich habe das in HM485 auch schon eingebaut und habe den Eindruck, dass es funktioniert.
Das "no strict refs" wie z.B. in CallFn have ich absichtlich weggelassen. Meiner Meinung nach ist es besser, wenn man saubere Funktionsreferenzen verwendet. Also z.B.:

PrioQueue_add(\&readingsSingleUpdate, [$hash, $name, $value, 1]);
PrioQueue_add(sub {$hash->{'AttrList'} = $attrlist}, [$hash]);


Ich hatte erst eine leicht andere Version, so dass der nächste Eintrag der PrioQueue erst abgearbeitet wird, wenn kein Eintrag aus $intAt ansteht. Allerdings hatte das zur Folge, dass das sehr stockend wurde. Irgendwas war auf meinem Testsystem immer los.
Ich erwarte nicht unbedingt, dass das in fhem.pl eingebaut wird. Ich kann das auch relativ einfach mit InternalTimer "lokal" nachbauen.

Durch die nähere Betrachtung mit HandleTimeout sind ein paar Fragen aufgekommen:

  • Inwiefern ist der sort am Anfang stable? Ich war immer davon ausgegangen, dass die per InternalTimer gestarteten Funktionen in der Reihenfolge des Eintrags ausgeführt werden, wenn sie die gleiche Zeit haben. Ich hatte bei genaueren Tests jedoch den Eindruck, dass das nicht immer der Fall ist.
  • Gibt es einen Grund, warum die Schleife nicht verlassen wird, wenn $tim > $now ? Es kann danach ja nichts mehr kommen, was abgearbeitet werden muss und $nextat kann sich auch nicht mehr ändern.
  • Wozu ist es gut, immer ein Mindest-Delay von 10ms zu haben? (Oder habe ich das falsch verstanden?)

Gruß,
    Thorsten
FUIP

Wuppi68

#9
Hallo Thorsten,

meinst Du so etwas wie unter Open VMS Queue Manasger http://www0.mi.infn.it/~calcolo/OpenVMS/ssb71/6015/6017p037.htm

fände ich persönlich auch Mega Cool

Wir haben früher bei uns in der IT die Queues richtig geliebt

edit:

gerade gesehen ... zwar alt, aber könnte gehen

http://search.cpan.org/~cberry/VMS-Queue-0_58/Queue.pm
Jetzt auf nem I3 und primär Homematic - kein Support für cfg Editierer

Support heißt nicht wenn die Frau zu Ihrem Mann sagt: Geh mal bitte zum Frauenarzt, ich habe Bauchschmerzen

Thorsten Pferdekaemper

Hi Wuppi68,
so kompliziert muss es ja nicht sein. Ich meinte es so, wie ich es implementiert habe, also ein Post vor Deinem.
Gruß,
   Thorsten
FUIP

rudolfkoenig

Habs eingecheckt, aber etwas abgespeckt und umformatiert (damit ich es spaeter noch verstehe), und statt array pointer mit einem Argument (damit es aehnlich wie InternalTimer ist).
Habs etwas getestet und dabei ist ein kleiner Bug aufgefallen: minNum ist besser als min.

ZitatInwiefern ist der sort am Anfang stable?
Keine Ahnung, vmtl. gar nicht. Verursacht das irgendwo Probleme?

ZitatGibt es einen Grund, warum die Schleife nicht verlassen wird, wenn $tim > $now ?
Vermutlich koennte man das tun, sobald $nextat gesetzt ist.

ZitatWozu ist es gut, immer ein Mindest-Delay von 10ms zu haben?
Keine Ahnung. Laut "svn blame fhem.pl":
2271 martinp876   $now += 0.01;# need to cover min delay at least

Kommentar dazu:
Zitatr2271 | martinp876 | 2012-12-06 21:44:21 +0100 (Thu, 06 Dec 2012) | 1 line

bug in timer calculation
Martin? Sonstwer? Sonst fliegt es raus :)

krikan

Zitatr2271 | martinp876 | 2012-12-06 21:44:21 +0100 (Thu, 06 Dec 2012) | 1 line

bug in timer calculation

Thread dazu https://groups.google.com/forum/#!topic/fhem-users/y_rlL8POnlY/discussion

Thorsten Pferdekaemper

Hi Rudi,
diese Zeile hier...

  $nice =   0 if(not defined($nice) || !looks_like_number($nice));

...ist nicht so gut. Das "||" bindet stärker als "not". (Siehe https://perldoc.perl.org/perlop.html#Operator-Precedence-and-Associativity). Daher sitzen die Klammern implizit so:

if(not (defined($nice) || !looks_like_number($nice)));

Wenn also $nice nicht übergeben wird (was bei mir der Normalfall ist), dann hat man

not (false || !false) => not(false || true) => not true => false

$nice bleibt also undef und es hagelt Probleme.
Ich denke, dass es in Perl besser ist, entweder not/or/and etc zu benutzen oder !/||&&, aber das nicht zu mischen.
Gruß,
   Thorsten
FUIP

Thorsten Pferdekaemper

...und noch was: Das mit der Array-Referenz war Absicht, damit man das auch einfach mit mehreren Parametern aufrufen kann. Ich habe es mit Deiner Version versucht, aber nicht hinbekommen. Natürlich geht es mit einem kleinen Wrapper, aber das ist schon sehr hässlich.
Gruß,
   Thorsten
FUIP