Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Kapitel 2: Task-Scheduling

Lernziele

Nach diesem Kapitel sollten Sie in der Lage sein:


1. Einleitung und Definition

Scheduling bezeichnet im Kern die zeitliche Planung und Zuweisung von Aufgaben (Tasks) an verfügbare Ressourcen (Prozessoren). Während wir Scheduling aus vielen Bereichen der Informatik kennen (z. B. Prozess-Scheduling im Betriebssystem oder Job-Planung in Clustern), konzentriert sich das Task-Scheduling speziell auf die Planung von Teilaufgaben, die untereinander Abhängigkeiten aufweisen können.

Die zentrale Fragestellung

Parallele Tasks werden von parallelen Prozessoren ausgeführt. Der Scheduler muss entscheiden: Welcher Prozessor führt welchen Task zu welchem Zeitpunkt aus?

Ziele des Schedulings

Bei der Entwicklung eines Schedulers müssen zwei oft gegensätzliche Ziele optimiert werden:

  1. Minimierung der Gesamtausführungszeit (MakeSpan): Das Programm soll so schnell wie möglich fertiggestellt werden.

  2. Effizienter Speicherverbrauch: Die parallele Ausführung darf den Speicherbedarf nicht unkontrolliert vergrößern (z. B. durch das unnötige Duplizieren von Ressourcen für jeden Thread).


2. Klassifizierung: Statisch vs. Dynamisch

Je nachdem, wann die Entscheidungen getroffen werden, unterscheidet man zwei Ansätze:

MerkmalStatisches SchedulingDynamisches Scheduling
ZeitpunktVOR der Ausführung (Offline)WÄHREND der Ausführung (Online)
VoraussetzungenKenntnisse über Laufzeiten, Kommunikation und Abhängigkeiten müssen vorliegen.Die Zukunft des Programms (folgende Tasks) ist unbekannt.
ModellierungOft durch präzise Task-Graphen (DAGs).Oft durch abstrakte Thread-Modelle (wie bei Blumofe und Leiserson).
AnwendungHochgradig reguläre Probleme, Echtzeitsysteme.Unregelmäßige Algorithmen, unbekannte Eingabedaten.

3. Grundlagen der Performance-Messung

Um die Qualität eines Schedulers (insbesondere im dynamischen Umfeld) zu bewerten, werden zwei theoretische Maße herangezogen:

Ein optimaler Greedy-Scheduler für PP Prozessoren garantiert eine Laufzeit von TPT1P+TT_P \leq \frac{T_1}{P} + T_\infty.


4. Grundlagen und der Busy-Leaves-Algorithmus

Beim dynamischen Task-Scheduling ist die Struktur des Programms (welche Tasks als Nächstes entstehen) zur Laufzeit nicht bekannt. Das Ziel eines Schedulers ist es, die Gesamtlaufzeit (TPT_P) zu minimieren und gleichzeitig den Speicherverbrauch effizient zu begrenzen. Die theoretische Basis hierfür bildet das Modell von Robert D. Blumofe und Charles E. Leiserson (JACM 1999).

4.1 Das Task-Modell nach Blumofe und Leiserson

Um parallele Berechnungen mathematisch zu erfassen, nutzen Blumofe und Leiserson ein spezifisches Modell für Multithreaded-Programme:

4.2 Der Busy-Leaves-Algorithmus

Der Busy-Leaves-Algorithmus ist ein idealisierter, zentralisierter Algorithmus, der beweist, dass man parallele Berechnungen mit optimalem Speicherverbrauch ausführen kann.

Funktionsweise

Der Algorithmus verwaltet alle bereiten Threads in einem globalen Thread-Pool.

  1. Blatt (Leaf): Ein Thread im Spawn-Baum, der aktuell keine aktiven Kinder hat.

  2. Busy-Regel: Der Algorithmus stellt sicher, dass zu jedem Zeitpunkt alle Blätter des Spawn-Baumes “busy” sind (d.h. von einem Prozessor bearbeitet werden).

  3. Die drei Regeln:

    • (1) Spawn: Wenn ein Prozessor einen neuen Kind-Thread erzeugt, wird der Eltern-Thread in den globalen Pool zurückgelegt. Der Prozessor arbeitet am Kind-Thread weiter. Der Kind-Thread ist jetzt das neue Blatt und ist sofort “busy”.

    • (2) Stall: Wenn ein Thread auf das Ergebnis eines Kindes warten muss, wird er in den Pool zurückgelegt (als blockiert markiert) und der Prozessor wird frei.

    • (3) Die: Wenn ein Thread endet, prüft der Prozessor, ob der Eltern-Thread dadurch zum Blatt wird (d.h. keine weiteren lebenden Kinder hat). Falls ja, holt er den Eltern-Thread aus dem Pool und arbeitet an ihm weiter.

Beispiel: Parallele Summe mit 2 Prozessoren

Betrachten wir die parallele Berechnung von sum(a, b, c, d, e, f g, h). Das Programm erzeugt 5 Tasks:

Task Graph Sum

Figure 1:Möglicher Task-Graph für die Summenbildung

Ein möglicher TASK-Graph ist in Abblidung 1 dargestellt. Wir haben jeweils eine Einheit für den Start und eine für das Erzeugen eines Tasks.

Der Ablauf nach den Busy-Leaves-Regeln:

SchrittP1P2Queue
1T1T_1: Start--
2T1T_1: Spwan T2T_2--
3T2T_2: StartT1T_1 Spawn T5T_5-
4T2T_2: Spawan $T_3T5T_5: StartT1(b)T_1(b)
5T3T_3: StartT5T_5:Spawn T6T_6T1(b)T_1(b), T2T_2
6T3:z1=a+bT_3: z_1 = a+bT6T_6: StartT1(b)T_1(b), T2T_2, T5T_5
7T2T_2: Spwan T4T_4T6:z3=e+fT_6: z_3= e+fT1(b)T_1(b), T5T_5
8T4T_4: StartT5T_5: Spawn T7T_7T1(b)T_1(b), T2T_2
9T4:z2=c+dT_4: z_2=c+dT7T_7: StartT1(b)T_1(b), T2T_2, T5T_5
10T2:z5=z1+z2T_2: z_5 = z_1+z_2T7:z4=g+hT_7: z_4= g+hT1(b)T_1(b), T5T_5
11-T5:z6=z3+z4T_5: z_6 = z_3+z_4T1(b)T_1(b)
12-T1:sum=z5+z6T_1: sum = z_5 + z_6-

Beobachtungen:

Das Problem der Skalierbarkeit

Der globale Task-Pool ist ein zentraler Flaschenhals: Alle Prozessoren konkurrieren um Zugriff auf denselben Pool. Bei vielen Prozessoren wird dies zum Engpass, weshalb Work-Stealing als dezentrale Umsetzung entwickelt wurde.

4.3 Speicher-Restriktionen (Space Bounds)

Die größte Stärke des Busy-Leaves-Ansatzes ist die Garantie über den Speicherplatzbedarf. In vielen parallelen Systemen kann der Speicherbedarf explodieren, wenn zu viele Threads gleichzeitig begonnen werden.

Blumofe und Leiserson beweisen für strikte Berechnungen: Die Menge an Speicher SPS_P, die bei der Ausführung mit PP Prozessoren benötigt wird, ist begrenzt durch:

Definition S1S_1 (Sequentielle Stapeltiefe): S1S_1 ist der maximale Speicherbedarf bei rein sequentieller Ausführung auf einem Prozessor — im Wesentlichen die maximale Tiefe des Aufrufstapels multipliziert mit dem Speicher pro Stackframe. Bei rekursiven Programmen entspricht S1S_1 der maximalen Rekursionstiefe. Für fib(n) wäre S1=O(n)S_1 = O(n).

SPS1PS_P \leq S_1 \cdot P

Warum ist das so?

Dies stellt sicher, dass ein Programm, das auf einem Rechner sequentiell läuft, auch auf einem parallelen System mit PP Prozessoren nicht am Speicher scheitert (vorausgesetzt, man hat PP-mal so viel Speicher zur Verfügung).

5. Herleitung: Work-Stealing (Praxis)

Der Busy-Leaves-Algorithmus ist theoretisch perfekt, benötigt aber einen zentralen Pool für freie Blätter, was bei vielen Prozessoren zum Flaschenhals wird. Work-Stealing ist die praktische, dezentrale Umsetzung dieser Idee.

5.1 Das Prinzip der Deque

Jeder Prozessor führt eine lokale Datenstruktur, eine Deque (Double-Ended Queue), in der bereite Threads gespeichert werden:

  1. LIFO lokal (Stack-Prinzip): Ein Prozessor arbeitet lokal an seinen Tasks. Neue Tasls (durch Spawns) werden “oben” auf die Deque gelegt und sofort bearbeitet. Das entspricht der Busy-Leaves-Regel (das neueste Blatt wird sofort “busy”).

  2. FIFO beim Stehlen (Queue-Prinzip): Läuft ein Prozessor leer, wird er zum Dieb. Er wählt ein Opfer-Prozessor gleichmäßig zufällig (uniform random) aus und stiehlt einen Task vom anderen Ende (unten) der Deque. Die Zufälligkeit ist kein Zufall: Die Laufzeitgarantie des Algorithmus basiert direkt darauf, dass kein deterministisch schlechtes Opfer gewählt werden kann.

  3. Wird ein blockierter Task wieder Rechenbereit, kommt er in die Queue des Prozessors, der die Abhängigkeit aufgelöst hat.

Beispiel: Work-Stealing bei parallelem Fibonacci fib(4)

Eine Möglichkeit, Fibonacci zu berechnen, ist dies rekursiv zu tun;

int fib(int n) {
     if (n < 2) return n;
      int  x = fib(n - 1); 
      int  y = fib(n - 2); 
      return  x + y; 
} 

Ein möglicher Task-Graph ist in Abbildung zu sehen.

Dag für (fib(4))

Figure 2:DAG für fib(4).

Es ergibt sich bei drei Prozessen, P1,P2 und P3 und einem Work-Stealing folgendes Bild:

SP1P2P3P1-QueueP2-QueueP2-QueueBlocked
1T1T_1 : start-----
2T1T_1: spawn T2T_2--[T1][T_1]--
3T2T_2: startT1T_1: spawn T7T_7----T1T_1
4T2T_2: spawn T3T_3T7T_7: start-[T2][T_2]--T1T_1
5T3T_3: startT7T_7: spawn T8T_8T2T_2: spwan T6T_6-[T7][T_7]-T1,T2T_1,T_2
6T3T_3 spwan T4T_4T8T_8 startT6T_6: start[T3][T_3][T7][T_7]-T1,T2T_1,T_2
7T4T_4: startT8T_8: return 1T6T_6: return 1[T3][T_3][T7][T_7]-T1,T2T_1,T_2
8T4T_4: return 1T7T_7: spawn T9T_9T3T_3: spawn T5T_5---T1,T2,T3,T7T_1,T_2,T_3,T_7
9-T9T_9: startT5:T_5: start---T1,T2,T3,T7T_1,T_2, T_3,T_7
10-T9T_9: return 0T5T_5 return 0-T7T_7T3T_3T1,T2T_1,T_2
11-T7:0+1T_7: 0+1T3:0+1T_3: 0+1--T2T_2T1T_1
12--T2:1+1T_2: 1+1--T1T_1-
13--T1:2+1T_1: 2+1----

Was dieses Beispiel zeigt:

5.2 Warum vom anderen Ende stehlen?

Das Stehlen vom “alten” Ende der Deque hat zwei entscheidende Vorteile:

  1. Größere Arbeitspakete: Threads am unteren Ende der Deque sind meist “höher” im Aktivierungsbaum angesiedelt. Sie repräsentieren oft große Teilbäume. Ein Diebstahl bringt also viel Arbeit, was die Anzahl zukünftiger Diebstähle minimiert.

  2. Konfliktvermeidung: Da der Besitzer oben arbeitet und der Dieb unten stiehlt, gibt es seltener Synchronisationskonflikte beim Zugriff auf die Deque.

5.3 Warum Work-Stealing statt Work-Sharing?

Work-Stealing erfüllt die Busy-Leaves-Eigenschaft und ist ein Greedy-Algorithmus: Kein Prozessor bleibt unnötig idle, solange noch Arbeit vorhanden ist. Durch die verteilten Deques (eine pro Prozessor statt einer gemeinsamen Queue) müssen Threads nicht gleichzeitig auf eine gemeinsame Datenstruktur zugreifen — kein globales Locking, keine globale kritische Region. Die Performance ist dadurch in der Regel deutlich besser als beim Work-Sharing.

Inzwischen gibt es viele Optimierungen über den ursprünglichen Algorithmus hinaus:

5.4 Laufzeitgarantie des randomisierten Work-Stealing

Das zentrale Ergebnis von Blumofe und Leiserson: Der randomisierte Work-Stealing-Algorithmus erzielt für voll-strikte Berechnungen eine erwartete Laufzeit von:

E[TP]=O ⁣(T1P+T)E[T_P] = O\!\left(\frac{T_1}{P} + T_\infty\right)

Das bedeutet: Work-Stealing erreicht linearen Speedup — es wird praktisch so schnell wie theoretisch maximal möglich. Der Erwartungswert ist über die Zufälligkeit der Opferwahl, nicht über die Eingabe. Mit hoher Wahrscheinlichkeit weicht die tatsächliche Laufzeit kaum vom Erwartungswert ab.

Warum nur für voll-strikte Berechnungen? Der Beweis nutzt die Struktur der Deque aus (Lemma 4 im Paper): In einer voll-strikten Berechnung ist die Deque jedes Prozessors immer ein aufsteigender Pfad im Spawn-Baum (jeder Eintrag ist Elternteil des darunter liegenden). Dieses strukturelle Invariant bricht für allgemeinere Berechnungen.


6. Praxisbezug: Work-Stealing in realen Systemen

Die theoretischen Ergebnisse von Blumofe und Leiserson sind direkt in realen Parallelisierungsframeworks umgesetzt worden:

SystemSpracheBesonderheit
Cilk / Cilk PlusC/C++Direkte Implementierung des Papers; Schlüsselwörter cilk_spawn, cilk_sync
Java ForkJoinPoolJavaStandardbibliothek seit Java 7; Grundlage für parallele Streams
Intel TBBC++task_group, parallel_for — Work-Stealing-basiert
OpenMP TasksC/C++/Fortran#pragma omp task + #pragma omp taskwait

Gemeinsames Muster: Alle diese Systeme erlauben es, Berechnungen rekursiv in kleinere Tasks zu zerlegen (spawn/fork), auf Ergebnisse zu warten (sync/join), und überlassen das Scheduling vollständig dem Laufzeitsystem.

Beispiel in Cilk (paralleles Fibonacci):

int fib(int n) {
    if (n < 2) return n;
    int x = cilk_spawn fib(n - 1);  // spawn: Elternthread in Deque, weiter mit fib(n-1)
    int y = fib(n - 2);              // läuft inline
    cilk_sync;                       // stall: warte auf gespawnten Thread
    return x + y;
}

Der Scheduler entscheidet zur Laufzeit, welche fib-Aufrufe parallel ausgeführt werden — ohne dass der Programmierer Threads manuell verwalten muss.

Ausblick auf die nächste Einheit: In der folgenden Einheit wird OpenMP Tasks vorgestellt — die in der Praxis am weitesten verbreitete Umsetzung dieser Idee. Genau das obige Fibonacci-Beispiel wird dort mit #pragma omp task und #pragma omp taskwait implementiert. Die theoretischen Konzepte aus diesem Kapitel (Spawn, Stall, Work-Stealing-Deque) sind dann direkt auf die OpenMP-Schlüsselwörter abbildbar.


Zusammenfassung

Das Modell von Blumofe und Leiserson zeigt, dass durch die konsequente Bearbeitung von Blättern (Busy-Leaves) die Speicherkomplexität im Griff bleibt. Das Work-Stealing übersetzt dies in ein hocheffizientes, dezentrales System, das die Kommunikation minimiert und die Lokalität maximiert.

Teil 2: Statisches Task-Scheduling (Heuristiken)

2.1 Modellierung mit Kommunikationskosten

Im statischen Scheduling sind alle Parameter vorab bekannt.

2.2 Prioritätszuweisung: t-level und b-level

Viele Heuristiken basieren auf zwei zentralen Attributen, die den “Rang” eines Tasks im DAG beschreiben:

2.3 List-Scheduling-Heuristiken (BNP-Klasse)

Da das optimale statische Scheduling NP-hart ist, nutzen wir Heuristiken, um bereite Tasks aus einer Prioritätsliste zuzuweisen. Die folgenden Algorithmen gehören zur BNP-Klasse (Bounded Number of Processors — feste Prozessorzahl vorgegeben). Wir betrachten im folgenden den in Abbildung 3 dargestellten Graphen.

DAG für einen Graphen mit 9 Knoten

Figure 3:Beispiel DAG.

Für diesen Graphen ergibt sich:

Knotenb-Levelt-Levelstatic-levelALAP
A260190
B214185
C242152
D1421012
E174129
F1015816
G1210614
H15111011
I323323

A. HLFET (Highest Level First with Estimated Times)

  1. Berechne für jeden Knoten das Static Level (SL): der längste Pfad vom Knoten bis zum Ausgangsknoten, nur mit Rechenzeiten, ohne Kantenkosten.

  2. Priorisiere Tasks mit dem höchsten SL.

  3. Weise jeden bereiten Task dem Prozessor zu, auf dem er am frühesten starten kann.

Für den HLFET-Scheduler ergibt sich der in Abbildung dargestellte Schedule-

Gant-Chart für HLFET Algorithmus

Figure 4:Ablauf für den HLFET-Algorithmus für den in Abbildung 3 dargestellten Task-Graphen.

B. ISH (Insertion Scheduling Heuristic)

  1. Berechne das b-level (inkl. Kommunikationskosten) zur Prioritätsvergabe.

  2. Weise den nächsten Task zunächst dem Prozessor mit der frühesten Startzeit zu.

  3. Insertion-Schritt: Prüfe, ob der Task in eine Leerstelle (idle slot) eines anderen Prozessors eingefügt werden kann, ohne dessen nachfolgende Tasks zu verzögern — falls ja, bevorzuge diese Lösung.

Gant-Chart fürISH Algorithmus

Figure 5:Ablauf für den ISH-Algorithmus für den in Abbildung 3 dargestellten Task-Graphen.

C. MCP (Modified Critical Path)

  1. Berechne die ALAP-Zeit (As Late As Possible) für jeden Knoten: der spätestmögliche Startzeitpunkt, zu dem der Knoten noch ohne Verlängerung des Gesamtplans beginnen darf.

  2. Erzeuge für jeden Knoten eine Liste aller ALAP-Zeiten auf dem Pfad vom Knoten zum Ausgangsknoten.

  3. Priorisiere Tasks lexikographisch aufsteigend nach dieser Liste (die “dringendsten” Tasks zuerst).

  4. Weise jeden Task dem Prozessor zu, auf dem er am frühesten starten kann.

Gant-Chart für MCP Algorithmus

Figure 6:Ablauf für den MCP-Algorithmus für den in Abbildung 3 dargestellten Task-Graphen.

D. ETF (Earliest Time First)

  1. Berechne für jeden bereiten Task auf jedem verfügbaren Prozessor die frühestmögliche Startzeit (EST).

  2. Die EST berücksichtigt sowohl, wann der Prozessor frei ist, als auch, wann die Daten der Vorgänger (inkl. Kommunikation) eintreffen.

  3. Wähle das Paar (Task, Prozessor) mit dem global kleinsten EST-Wert.

Gant-Chart für ETF Algorithmus

Figure 7:Ablauf für den ETF-Algorithmus für den in Abbildung 3 dargestellten Task-Graphen.

2.4 Empirischer Vergleich (Kwok & Ahmad, 1999)

Das Paper von Kwok und Ahmad vergleicht alle BNP-Algorithmen systematisch auf verschiedenen Benchmark-Graphen mit CCR {0.1,1.0,10.0}\in \{0.1, 1.0, 10.0\}.

Ranking nach Scheduling-Qualität (BNP-Klasse):

RangAlgorithmusStärke
1MCPBeste Gesamtperformance durch ALAP-Listenprioritäten
2ISHEffektive Ausnutzung von Idle-Slots
3DLSGute dynamische Anpassung
4HLFETEinfach, aber ignoriert Kommunikation
5ETFGute Qualität, aber hoher Rechenaufwand
6LASTSchlechteste Performance

Zentrale Erkenntnisse:

2.5 Zusammenfassung