Forum: PC-Programmierung Optimierungsproblem: Gerichteter Graph zu verschachtelte Liste aus Parallelen und Sequenziellen jobs


von Daniel A. (daniel-a)


Lesenswert?

Bei ein m JS Programm, an dem ich gerade Arbeite, hänge ich gerade etwas 
fest.
Ich habe Klassen für Jobs, Parallelausführbare Joblisten, und 
sequenziell ausführbare Joblisten, und die Joblisten erben von der Job 
klasse, sind also verschachtelbar.

An den meisten Stellen meines Programms war das sehr Praktisch, da hatte 
ich rekursive Funktionen, die jeweils eine Sequenzielle oder Parallele 
Liste oder ein Job zurückgegeben haben, und das dann zusammen setzten.

Aber jetzt habe ich eine Situation, wo ich eine Reihe von Jobs habe, und 
ich weiss, welcher Job von welchen andern Abhängig sind, ein gerichteter 
azyklischer Graph. Daraus muss ich nun irgendwie diese Sequenziellen und 
Parallelen Joblisten machen.

Ich weiss, dass es da vermutlich keine Perfekte Lösung gibt, aber es 
gibt mehr oder weniger akzeptable Lösungen. Die Trivialste Lösung wäre 
die Jobs einfach zu sortieren, und dann eine Sequenz daraus zu machen. 
Aber eigentlich würde ich gerne so viel wie möglich parallel machen, und 
möglichst wenig nacheinander.
1
class Job {};
2
class JobList extends Job {};
3
class ParallelJobList extends JobList {
4
  constructor(jobs){
5
    this.list = new Set(jobs);
6
  }
7
  add(job){
8
    this.list.add(job);
9
  }
10
};
11
class SequentialJobList extends JobList {
12
  constructor(jobs){
13
    this.list = [...(jobs??[])];
14
  }
15
  add(job){
16
    this.list.push(job);
17
  }
18
};
19
20
function generate(){
21
  const jobs = []; // Array holding jobs
22
  const depends = Symbol();
23
  const dependants = Symbol();
24
25
  // Some code generating the jobs here
26
  // job[depends] is a Set containing a reference to all Jobs the Job depends on.
27
  // job[dependants] is a Set containing a reference to all Jobs which depend on this Job.
28
29
  // TODO: Convert this into JobLists somehow
30
31
  return joblist;
32
}

Meine erste Idee war, das in Gruppen einzuteilen und zu Ordnen, 
basierend auf den Jobs ohne Vorgängern:
1
  const top_nodes = new Set();
2
  for(const a of jobs)
3
    if(!a[dependants])
4
      top_nodes.add(a);
5
  const level = Symbol();
6
  const groups = Symbol();
7
  function walk_tree(group, node, covered=new Set(), i=0){
8
    if(covered.has(node))
9
      throw new Error("Couldn't resolve cyclic dependency between parallel actions");
10
    node[level] = i;
11
    (node[groups]??=new Set()).add(group);
12
    covered = new Set([...covered, node]);
13
    for(const dnode of node[dependants])
14
      walk_tree(group, dnode, covered, i+1);
15
  }
16
  let group = 0;
17
  for(const a of top_nodes)
18
    walk_tree(group++, a);

Aber irgendwie bringt mich das auch nicht weiter.

Als Beispiel, sowas könnte irgendwie so aussehen ( Mit () Sequenziell, 
{} Parallel ):
1
a <- d
2
b <- d
3
d <- e
4
d <- f
5
b <- f
6
g <- e
7
h <- i
8
9
{
10
  (i,h),
11
  (
12
    {
13
      (
14
        {a,b},
15
        d,
16
      ),
17
      g
18
    }
19
    {e,f}
20
  )
21
}

Habt ihr Ideen, wie ich das einigermassen Sinnvoll machen könnte?

: Bearbeitet durch User
von Der Freundliche Gast X. (Firma: mc.net) (friendly_offtopic)


Lesenswert?

Der Begriff den du suchst heißt:

Topologische Sortierung

https://de.wikipedia.org/wiki/Topologische_Sortierung

und was du dazu noch machen kannst: Knoten die von Haus aus nur 1 
Eingang und 1 Ausgang haben, logisch dem Vor-/Nachfolgeknoten zuordnen, 
da der quasi zwingend zu einer Sequenz gehört.

: Bearbeitet durch User
von A. S. (rava)


Lesenswert?

Ein manueller Ansatz ist, einfach stets alles in deiner sortierten 
Sequenz parallel auszuführen, was "bereit" ist, i.e. dessen 
Vorbedingungen erfüllt sind. Möglich ist das zum Beispiel mit 
threadsicheren queues und thread pools für die es in deiner Umgebung 
bestimmt APIs gibt.

https://en.wikipedia.org/wiki/Thread_pool

Wenn das System schlauer werden soll als das, musst du überlegen welche 
der jobs zu priorisieren sind. Das kann man eigentlich nur wirklich 
sinnvoll, wenn man die erwarteten Laufzeiten kennt oder bei sich 
wiederholenden Jobs diese Laufzeiten wenigstens messen und statistisch 
auswerten kann.

Das Thema statisches und dynamisches Scheduling ist aber ein Feld für 
sich. Um hier etwas empfehlen zu können, bräuchten wir mehr 
Informationen.

: Bearbeitet durch User
von Daniel A. (daniel-a)


Lesenswert?

Der Freundliche Gast X. schrieb:
> Topologische Sortierung

Topologische Sortierung geht schon mal in die richtige Richtung, das 
tönt fast genau wie das, was ich hier versuche. Nur dass ich nicht nur 
eine Liste, sondern verschachtelte Listen habe, aus Parallel und 
Sequenziell ausführbarem. Aber es ist ein guter Startpunkt, ich bin noch 
am schauen, was es da so alles gibt.

A. S. schrieb:
> Ein manueller Ansatz ist, einfach stets alles in deiner sortierten
> Sequenz parallel auszuführen, was "bereit" ist

Das kann ich leider nicht machen. Das ist teil eines recht komplexen 
Solvers, ich brauch das dort als Teil einer wichtigen Optimierung, an 
der Stelle kann ich das nicht anders aufbauen, da kommen auch noch 
weitere Optimierungen hinten dran die die Datenstruktur erwarten.

Ausserdem führt das Programm die Jobs auch nicht selbst aus. Das geht in 
etwa so:
* User gibt ein, was er gerne hätte.
* Das Programm findet raus, wie das gemacht werden kann.
* Die Klassen ParallelJobList, SequentialJobList und Job, haben eine 
Serializierungs- und Deserialisierungs-Methode. Das wird dann erstmal 
als JSON in der DB gespeichert.
* Die DB ID und das JSON wird an den Browsergesendet. Der hat die 3 
Klassen auch, und davon abgeleitete Klassen, die daraus ein UI aufbauen 
können.
* Der Nutzer sieht dann, was in welcher Reihenfolge ausgeführt werden 
würde.
* Der Nutzer gibt an, wann er es ausführen will, bestätigt es, und an 
den Server wird die Zeit und di ID des Ablaufs gesendet.
* Serverseitig wird dass dann wieder Deserialisiert, und in einem neuen 
Format serialisiert und an ein anderes Programm gesendet. Das andere 
Programm führt es dann aus.

Das ist eigentlich alles schon fertig, und geht eigentlich auch - Einige 
Abläufe sind nur noch etwas suboptimal, da sitze ich momentan dran. Und 
das ganze ist noch etwas komplexer als hier beschrieben.

von Daniel A. (daniel-a)


Lesenswert?

Das kann man zwar sicher noch verbessern, aber für meine Zwecke müsste 
es vorerst reichen: https://jsfiddle.net/gj9yn37s/3/

von A. S. (rava)


Lesenswert?

Daniel A. schrieb:
> Das kann ich leider nicht machen.

ich verstehe deinen Punkt nicht ganz. Natürlich kannst du den Plan so 
wie ich's beschrieben habe auch offline vorberechnen und dann an den 
user oder wegen mir auch an den solver geben.

Wenn du einen besseren Plan als das haben willst, dann ist das ein 
Zeichen dafür dass du weniger Kerne hast als parallel laufende Jobs. 
Dann brauchst du wie gesagt Informationen oder wenigstens Heuristiken 
über die Laufzeiten, um eine kluge Entscheidung treffen zu können.

Du kannst zum Beispiel annehmen, dass jeder Job gleich lange dauert. 
Aber das sollte eine bewusste Entscheidung sein.

: Bearbeitet durch User
von Daniel A. (daniel-a)


Lesenswert?

A. S. schrieb:
> ich verstehe deinen Punkt nicht ganz. Natürlich kannst du den Plan so
> wie ich's beschrieben habe auch offline vorberechnen und dann an den
> user oder wegen mir auch an den solver geben.

Sagen wir, wir haben folgendes:

a<-b
b<-c
d<-e
e<-f

Sangen wir nun, ich schaue, was momentan ausgeführt werden kann. Das 
wären a und d. Nun kommt der Knackpunkt. Wenn es sofort ausgeführt wird, 
weiss ich danach, ok, a ist durch, also kann nun b kommen, weil keine 
Vorgänger mehr. Aber da die Jobs nicht sofort ausgeführt werden, kann 
ich das nicht machen.

Ich könnte jetzt depth first oder breath first da durch gehen. Depth 
first gäbe das a->b->c->d->e->f. Breath first würde es 
[a,d]->[b,e]->[c,f]. Das ist am Ende genau das, was die Algorithmen zur 
Topologischen Sortierung auch machen, und auch mehr oder weniger das 
selbe, was ich oben sowieso schon gemacht habe. Ideal wäre hier nun aber 
[a->b->c, d->e->f], die 2 Sequenzen parallel. Mit einem Besseren 
Algorithmus kann man das auch heraus bekommen. Und genau nach solchen 
Suchte ich ja. Aber hat sich vorerst eh erledigt, der momentane Ansatzt 
geht bisher gut genug.

A. S. schrieb:
> Wenn du einen besseren Plan als das haben willst, dann ist das ein
> Zeichen dafür dass du weniger Kerne hast als parallel laufende Jobs.
> Dann brauchst du wie gesagt Informationen oder wenigstens Heuristiken
> über die Laufzeiten, um eine kluge Entscheidung treffen zu können.

Die Jobs sind komplett anders, als du dir das vorstellst. Das sind 
Sachen wie Server Runterfahren, Starten, Rebooten, checken ob Services 
laufen, Ressourcen in VMWare oder der HMC anpassen, Maintenance Mode 
setzen / beenden, etc. Das macht allerhand Dinge auf allerhand Servern, 
und die Dinge haben Abhängigkeiten zueinander. Wie lange das dauert kann 
man auch oft nicht sagen. Es kann auch sein, dass mal etwas nicht geht, 
und man es selbst machen muss, und den Job dann bestätigt.

Ich brauche die Datenstruktur einerseits im Solver zur weiteren 
Optimierung, und andererseits, zur Visualisierung. Die Einteilung in 
parallele und sequenzielle Blöcke macht es übersichtlicher, als nur 
einen Graphen anzuzeigen. Die Abläufe betreffen wichtige Produktive 
Systeme, da muss genau hingeschaut werden, dass da alles richtig ist, 
bevor man die bestätigt. Und im Programm, welches die nachher ausführt, 
sieht man auch genau, was läuft in der Jobkette, und kann auch Jobs 
Anhalten, überspringen, etc.

von A. S. (rava)


Lesenswert?

ah okay. Wenn du dich für BFS entschieden hast, meinen wir ja eh das 
gleiche.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.