Zum Inhalt springen

Scheduler

Der Scheduler führt Prozesse mit einem Work-Stealing-Design aus. Worker pflegen lokale Deques und stehlen voneinander, wenn sie untätig sind.

Der Scheduler arbeitet mit jedem Typ, der das Process-Interface implementiert:

type Process interface {
Init(ctx context.Context, method string, input payload.Payloads) error
Step(events []Event, out *StepOutput) error
Close()
}
MethodeZweck
InitProzess mit Entry-Methodenname und Eingabeargumenten vorbereiten
StepZustandsmaschine mit eingehenden Events vorantreiben, Yields in Output schreiben
CloseRessourcen freigeben

Der method-Parameter in Init spezifiziert welchen Einstiegspunkt aufgerufen werden soll. Eine Prozessinstanz kann mehrere Einstiegspunkte exponieren, und der Aufrufer wählt welchen er ausführen möchte. Dies dient auch als Verifikation, dass der Scheduler den Prozess korrekt initiiert.

Der Scheduler ruft Step() wiederholt auf, übergibt Events (Yield-Completions, Nachrichten) und sammelt Yields (Commands zum Dispatchen). Der Prozess schreibt seinen Status und alle Yields in den StepOutput-Buffer.

type Event struct {
Type EventType // EventYieldComplete oder EventMessage
Tag uint64 // Korrelationstag für Yield-Completions
Data any // Ergebnisdaten oder Nachrichten-Payload
Error error // Fehler wenn Yield fehlgeschlagen
}

Der Scheduler startet standardmäßig GOMAXPROCS Worker. Jeder Worker hat eine lokale Deque für cache-freundlichen LIFO-Zugriff. Eine globale FIFO-Queue behandelt neue Submissions und Cross-Worker-Transfers. Prozesse werden per PID für Nachrichtenrouting verfolgt.

flowchart TD
W[Worker braucht Arbeit] --> L{Lokale Deque?}
L -->|hat Items| LP[Von unten LIFO poppen]
L -->|leer| G{Globale Queue?}
G -->|hat Items| GP[Poppen + Batch-Transfer bis zu 16]
G -->|leer| S[Von zufälligem Opfer stehlen]
S --> SH[StealHalfInto Opfer-Deque]

Worker prüfen Quellen in Prioritätsreihenfolge:

PrioritätQuelleMuster
1Lokale DequeLIFO Pop, lock-frei, cache-freundlich
2Globale QueueFIFO Pop mit Batch-Transfer
3Andere WorkerHälfte von Opfer-Deque stehlen

Beim Poppen von global nehmen Worker ein Item und transferieren bis zu 16 weitere in Batch zu ihrer lokalen Deque.

Jeder Worker besitzt eine Chase-Lev Work-Stealing-Deque:

type Deque struct {
buffer atomic.Pointer[dequeBuffer]
top atomic.Int64 // Diebe stehlen hier (CAS)
bottom atomic.Int64 // Besitzer pusht/poppt hier
}

Der Besitzer pusht und poppt von unten (LIFO) ohne Synchronisation. Diebe stehlen von oben (FIFO) per CAS. Dies gibt dem Besitzer cache-freundlichen Zugriff auf kürzlich gepushte Items während ältere Arbeit an Stealer verteilt wird.

StealHalfInto nimmt die Hälfte der Items in einer CAS-Operation und reduziert Contention.

Bevor auf der Condition-Variable blockiert wird, spinnen Worker adaptiv:

Spin-CountAktion
< 4Enger Loop
4-15Thread yielden (runtime.Gosched)
>= 16Auf Condition-Variable blockieren
stateDiagram-v2
[*] --> Ready: Submit
Ready --> Running: CAS by worker
Running --> Complete: done
Running --> Blocked: yields commands
Running --> Idle: waiting for messages
Blocked --> Ready: CompleteYield
Idle --> Ready: Send arrives
ZustandBeschreibung
ReadyFür Ausführung eingereiht
RunningWorker führt Step() aus
BlockedWartet auf Yield-Completion
IdleWartet auf Nachrichten
CompleteAusführung beendet

Ein Wakeup-Flag behandelt Race-Conditions: Wenn ein Handler CompleteYield aufruft, während der Worker noch den Prozess besitzt (Running), setzt er das Flag. Der Worker prüft das Flag nach dem Dispatchen und reiht bei gesetztem Flag neu ein.

Jeder Prozess hat eine MPSC (Multi-Producer, Single-Consumer) Event-Queue:

  • Producer: Command-Handler (CompleteYield), Nachrichtensender (Send)
  • Consumer: Worker draint Events in Step()

Der Scheduler implementiert relay.Receiver um Nachrichten an Prozesse zu routen. Wenn Send() aufgerufen wird, schlägt er die Ziel-PID in der byPID-Map nach, pusht die Nachricht als Event in die Prozess-Queue und weckt den Prozess wenn idle durch Pushen in die globale Queue.

Bei Shutdown sendet der Scheduler Cancel-Events an alle laufenden Prozesse und wartet auf deren Abschluss oder Timeout. Worker beenden sich sobald keine Arbeit mehr übrig ist.