Sobes.tech
Senior

Wie funktionieren die Mechanismen async und await im Hintergrund?

sobes.tech KI

Antwort von AI

Async/await in Swift basiert auf strukturierter Nebenläufigkeit und verwendet Dispatch/Global Actors. Die zugrunde liegenden Mechanismen sind:

  1. Fortsetzungsstil: Asynchrone Funktionen werden in einem Fortsetzungsstil kompiliert. Anstatt sofort einen Wert zurückzugeben, wird die Kontrolle an die Swift-Laufzeit übergeben, die den aktuellen Zustand der Funktion (lokale Variablen, Ausführungspointer) speichert. Wenn die asynchrone Operation abgeschlossen ist, setzt die Laufzeit die Ausführung der Funktion aus dem gespeicherten Zustand fort.

  2. Aufgabe: Jeder Aufruf einer async-Funktion erstellt oder verbindet sich mit einer strukturierten Task. Die Task repräsentiert eine Arbeitseinheit und bildet eine Hierarchie. Die Eltern-Task wartet auf das Ende der Kind-Task.

  3. Job: Die Task wird in kleinere Arbeitseinheiten namens Job unterteilt. Ein Job stellt ein Codefragment dar, das auf einem Executor ausgeführt werden kann.

  4. Executor: Der Executor ist verantwortlich für das Starten eines Job. In der Standardbibliothek werden globale DispatchQueue als Executor für die meisten asynchronen Aufgaben verwendet. Spezifische Kontexte (z.B. MainActor) haben ihre eigenen spezialisierten Executor.

  5. Suspensionspunkte: await ist ein Suspensionspunkt. Beim Erreichen wird die Funktion ausgesetzt, die Kontrolle an den aufrufenden Code oder Executor übertragen und der aktuelle Job beendet. Die Swift-Laufzeit speichert den Zustand der Funktion.

  6. Wiederaufnahme: Wenn die asynchrone Operation, auf die await wartet, beendet ist, erstellt die Swift-Laufzeit einen neuen Job, um die Ausführung der angehaltenen Funktion fortzusetzen. Dieser Job wird in die Warteschlange des Executors gestellt.

  7. Abbruch: Die strukturierte Nebenläufigkeit unterstützt hierarchisches Abbrechen. Das Abbrechen eines Eltern-Task beendet automatisch alle ihre Kind-Task. Asynchrone Operationen können den Abbruchstatus prüfen und entsprechend reagieren.

Hier ein vereinfachtes Beispiel für die Kompilierung einer async-Funktion:

// Ursprüngliche async-Funktion
func fetchData() async -> Data {
    // Asynchrone Operation
    let data = await URLSession.shared.data(from: url).0
    return data
}

// Vereinfachte Darstellung der Kompilierung
// Anstelle der Rückgabe akzeptiert die Funktion eine Continuation
func fetchData(_ continuation: Continuation<Data, Error>) {
    // Anfangscode der Funktion
    let url = URL(string: "...")! // Beispiel
    
    // Starten der asynchronen Operation
    URLSession.shared.data(from: url) { data, _, error in
        // Dieser Abschluss wird nach Abschluss der asynchronen Operation ausgeführt
        if let data = data {
            // Ergebnis an die Continuation übergeben, Funktion wieder aufnehmen
            continuation.resume(returning: data)
        } else if let error = error {
            continuation.resume(throwing: error)
        } else {
            // Behandlung anderer Fälle
            continuation.resume(throwing: NSError(domain: "", code: 0, userInfo: nil))
        }
    }
    // Funktion endet hier, wartet auf den Aufruf von resume der Continuation
}

Await ist im Wesentlichen syntaktischer Zucker für die Verwaltung von Continuation und die Planung von Job auf dem Executor. Das Aussetzen blockiert den Thread nicht direkt, sondern ermöglicht es dem Executor, andere Jobs auszuführen.