1. async let: was das ist und was man berhaupt parallelisieren kann
Wenn Sie bereits irgendeinen Netzwerkcode geschrieben haben, ist Ihnen sicher ein seltsames Gefhl aufgefallen: Der Prozessor scheint fast nichts zu tun, aber das Programm denkt trotzdem „eine Ewigkeit lang nach“. Das ist normal: Das Netzwerk ist Warten. Und wenn wir mehrere Anfragen haben (zum Beispiel zwei Buch-ids), kann sich das Warten wie eine Matrjoschka stapeln: Wir warten auf die erste Antwort, erst dann starten wir die zweite, und so weiter.
Das Problem ist nicht, dass Swift „langsam“ wre, sondern dass wir das Warten selbst sequentiell aufgebaut haben. In asynchronem Code sieht die Laufzeit oft aus wie eine Schlange im Caf: Wenn Sie nacheinander anstehen, ist die Gesamtzeit die Summe der Wartezeiten; wenn Sie aber Kaffee und Croissant gleichzeitig an zwei Fenstern bestellt haben (falls das Caf das berhaupt anbietet), dann liegt die Gesamtzeit nher am Maximum aus beiden Wartezeiten.
async let ist genau dieses „zweite Fenster“, allerdings mit sehr strengen Regeln fr Sicherheit und die Lebensdauer von Aufgaben.
Was ist async let
async let sieht wie eine gewhnliche Konstante aus, verhlt sich aber anders: Der Ausdruck rechts wird in einer eigenen Kindaufgabe ausgefhrt, die sofort zu arbeiten beginnt, sobald die Ausfhrung diese Zeile erreicht. Das ist Teil des Modells structured concurrency: Kindaufgaben bilden einen „Aufgabenbaum“, und der bergeordnete Kontext ist dafr verantwortlich, dass die Kinder nicht eigenmig davonlaufen.
Wichtig sind drei Eigenschaften.
Erstens kann man async let nur in einem asynchronen Kontext deklarieren (in einer async-Funktion oder einem async-Closure), weil das Ergebnis ohnehin mit await abgeholt werden muss.
Zweitens gilt das Lesen eines async let-Werts als potenzieller Wartpunkt, deshalb verlangt der Compiler await, selbst wenn „es eigentlich schon fertig sein mte“.
Drittens ist das wirklich eine child task: Sie kann den Gltigkeitsbereich, in dem sie erzeugt wurde, nicht berleben.
Mini-Beispiel „an den Fingern abzhlen“:
import Foundation
func slowNumber(_ x: Int) async -> Int {
try? await Task.sleep(nanoseconds: 200_000_000)
return x
}
func demo() async -> Int {
async let a = slowNumber(1)
async let b = slowNumber(2)
return await a + await b
}
Achten Sie auf den Stil: a und b haben parallel „gestartet“, und await steht an der Stelle, an der wir das Ergebnis wirklich verwenden wollen.
Regel der Unabhngigkeit: Was man parallelisieren kann und was nicht
Wenn Entwickler async let zum ersten Mal sehen, kommt schnell der Wunsch auf, „alles berhaupt zu parallelisieren“: Datei lesen, Datei schreiben, Anfragen, Repository aktualisieren, in die Konsole ausgeben ... und irgendwo hier beginnen Ihre knftigen Bugreports zu weinen.
Damit async let korrekt und ntzlich ist, mssen die Operationen zu Beginn datenunabhngig sein. Das heit, wir knnen parallel starten:
- „hol das Buch mit id=10“
- „hol das Buch mit id=20“
weil die zweite Anfrage kein Ergebnis der ersten bentigt.
Aber wir knnen nicht parallel starten:
- „hole das Authentifizierungstoken“
- „fhre die Anfrage mit diesem Token aus“
weil der zweite Schritt ohne das Ergebnis des ersten nicht beginnen kann: Das ist bereits eine Abhngigkeit, also ein sequentielles await.
Ein weiterer „heikler“ Fall von Unabhngigkeit ist, wenn Sie in Wirklichkeit ein gemeinsames mutierbares Objekt teilen. async let erzeugt eine Art @Sendable-artige Arbeit, und der Compiler wird uerst misstrauisch gegenber Versuchen sein, erfasste Variablen zu mutieren, weil das ein mglicher Datenrace ist. Und das ist gut so: Swift versucht Sie an dieser Stelle vor „es funktioniert manchmal, aber nur dienstags“ zu bewahren.
3. Lifetime und Scope: Warum man async let nicht „fr spter liegen lassen“ kann
Hier passiert normalerweise die Magie, die Anfngern wie ein „Sprachkaprize“ vorkommt. In Wirklichkeit ist das eine der strksten Garantien von Swift Concurrency.
Die Idee der structured concurrency
Structured concurrency sagt: Eine Kindaufgabe darf nicht lnger leben als ihr Elternteil (oder der Scope), in dem sie erstellt wurde. Das bedeutet: Man kann async let nicht hier starten und erst eine Minute spter irgendwo anders darauf warten, so wie man es zum Beispiel mit „Task-Handles“ (Task { ... }) in unstrukturiertem Stil tun knnte.
Bei async let ist die Lebensdauer streng auf den Gltigkeitsbereich begrenzt. Und genau deshalb drngt der Compiler Sie: Das Ergebnis muss im aktuellen Bereich abgeholt werden (await), sonst darf die Aufgabe nicht weiterexistieren.
Was passiert, wenn man gar nicht await-et
Interessant ist: Selbst wenn Sie ausdrcklich kein await gesetzt haben, lsst structured concurrency Sie die Aufgabe nicht einfach dauerhaft in den Hintergrund verschwinden. Beim Verlassen des Gltigkeitsbereichs wird Swift die Kindaufgaben implizit abbrechen und auf ihren Abschluss warten.
Der folgende Code sieht also aus wie „ich gehe sofort“, aber in Wirklichkeit wartet er, bis die Kindaufgaben beendet sind (oder abgebrochen und korrekt abgeschlossen wurden):
import Foundation
func go() async -> String {
async let fast = slowNumber(1)
async let slow = slowNumber(2)
return "nevermind..."
}
Deshalb ist async let kein „fire-and-forget“, sondern „jetzt starten, aber das Ergebnis in genau diesem Bereich unbedingt verarbeiten“.
4. Wie man await richtig verwendet und wo try hingehort
Jetzt lassen Sie uns die praktische Mechanik im Kopf zusammenbauen, ohne Philosophie.
Das Lesen von async let immer ber await
Da der Wert „spter ankommt“, muss jeder Zugriff darauf mit await markiert sein.
func sum() async -> Int {
async let a = slowNumber(10)
async let b = slowNumber(20)
let x = await a
let y = await b
return x + y
}
Praktischer Sammelpunkt: await (a, b)
Swift erlaubt es, sofort auf ein Paar von Werten zu warten (ein Tupel), was den Code visuell sauberer macht: Sie haben einen klaren „Sammelpunkt fr Ergebnisse“.
func sumTuple() async -> Int {
async let a = slowNumber(10)
async let b = slowNumber(20)
let (x, y) = await (a, b)
return x + y
}
Wenn ein Fehler mglich ist: try await, und die Reihenfolge ist fest
Wenn die Operation rechts einen Fehler werfen kann, verlangt das Warten auf das Ergebnis try await. Und zwar genau in dieser Reihenfolge: try await, nicht umgekehrt. Das ist die Regel fr async throws-Aufrufe in Swift.
import Foundation
enum DemoError: Error { case boom }
func mayThrow(_ ok: Bool) async throws -> Int {
if !ok { throw DemoError.boom }
return 42
}
func demoThrows() async -> Result<Int, Error> {
do {
async let v = mayThrow(false)
return .success(try await v)
} catch {
return .failure(error)
}
}
5. Mini-Schema: Wie async let ber die Zeit aussieht
Bevor wir das mit unserem CLI verbinden, ist es ntzlich, das Bild einmal zu sehen. Hier gibt es keine „echten Threads“ im Sinn von „ich kann garantieren, dass das auf zwei Kernen parallel luft“, aber im Hinblick auf das Warten auf das Netzwerk ist der Effekt genau der: Whrend eine Anfrage auf den Server wartet, kann auch die zweite auf den Server warten, statt in der Schlange hinter der ersten zu stehen.
flowchart TD
A["t0: Start der Funktion"] --> B["async let A: Anfrage #1 gestartet"]
B --> C["async let B: Anfrage #2 gestartet"]
C --> D["...wir erledigen vorbereitende Arbeit..."]
D --> E["await (A, B): Sammelpunkt der Ergebnisse"]
E --> F["t_end: Wir machen mit fertigen Daten weiter"]
Die Schlsselidee: Sie starten unabhngige Operationen frher, und await setzen Sie dort, wo man ohne Ergebnis nicht mehr weitermachen kann.
6. Anwendung in LibraryCLI: paralleles fetch von zwei Bchern
Jetzt machen wir genau das, wofr diese ganze Vorlesung da ist: Innerhalb eines (vereinfachten) fetch-many-Szenarios holen wir zwei Bcher parallel. Wichtig ist hier, nicht in Zukunftsthemen wie teilweisem Erfolg und Aggregation von Result pro Element abzudriften — das ist ein eigener Schritt. Heute machen wir die „einfache“ Version: Entweder wurden beide geladen, oder das Szenario endet mit einem Fehler.
Stellen wir uns vor, wir haben bereits:
- BooksEndpoint.details(id:), das ein URLRequest erzeugen kann;
- ApiClient.fetch(_:as:) async throws -> T;
- DTO und Domnenmodell.
Dann kann man eine „Einzelanfrage“ in eine kleine Funktion kapseln:
import Foundation
struct BookDTO: Decodable {
let id: Int
let title: String
}
func fetchBookDTO(id: Int, baseURL: URL, api: ApiClient) async throws -> BookDTO {
let request = try BooksEndpoint.details(id: id).makeRequest(baseURL: baseURL)
return try await api.fetch(request, as: BookDTO.self)
}
Und jetzt die Version „zwei Anfragen parallel“:
import Foundation
// kompilierbares Beispiel bei Vorhandensein von ApiClient/BooksEndpoint
func fetchTwo(ids: (Int, Int), baseURL: URL, api: ApiClient) async throws -> (BookDTO, BookDTO) {
async let a = fetchBookDTO(id: ids.0, baseURL: baseURL, api: api)
async let b = fetchBookDTO(id: ids.1, baseURL: baseURL, api: api)
return try await (a, b)
}
Hier passiert alles Wichtige.
async let erstellt Kindaufgaben, die sofort mit der Ausfhrung beginnen, sobald die Deklaration erreicht wird. Die Rckgabe (a, b) erfordert try await, weil jeder Zweig einen Fehler werfen kann, und wir das im Code ausdrcklich anerkennen mssen.
Warum wir die Ergebnisse genau in dieser Funktion zusammenfassen
Weil eine Kindaufgabe nicht lnger leben kann als der Gltigkeitsbereich, in dem async let deklariert wurde. Das zwingt den Code direkt dazu, Startpunkt und Wartestelle fr das Ergebnis in demselben verstndlichen Scope zu halten. Fr die Wartbarkeit ist das ein enormer Vorteil: weniger „auslaufende“ Hintergrundaufgaben, weniger berraschungen, weniger Situationen wie „es ldt immer noch irgendwas, aber wir sind schon raus“.
7. Nuance mit Closures: async let kann man nicht nach auen weiterreichen
Ein sehr hufiger Versuch von Anfngern ist: „Oh, ich speichere async let einfach in einer Variablen und gebe sie an einen Callback weiter.“ Aber async let ist nicht zum Weiterreichen nach auen gedacht. Er hat kein eigenes Handle-Objekt wie Task.
Mehr noch: async let darf nicht in einem escaping-Closure erfasst werden, weil das die Lebensdauer des Werts ber den Scope hinaus verlngern wrde, und das ist durch das Modell verboten. In der Diskussion dieser Regel wird betont, dass die Implementierung von async let optimiert werden kann (bis hin dazu, dass Teile der Struktur auf dem Stack liegen), und deshalb ist „Entkommen“ in einen escaping-Kontext unsicher.
In einem nicht-escaping-Async-Closure kann man es dagegen erfassen, aber await muss auch dort explizit sein:
import Foundation
func greet(_ f: () async -> String) async -> String { await f() }
func hello() async -> String {
async let name = slowNumber(123).description
return await greet { await name }
}
Ein solches Verhalten unterstreicht die Idee: await ist keine „technische Formalitt“, sondern ein Marker fr die Stelle, an der die Ausfhrung unterbrochen werden kann.
8. Typische Fehler bei der Arbeit mit async let
Fehler Nr. 1: async let verwenden und dann versuchen, ohne await auf den Wert zuzugreifen.
Das sieht aus wie „ich habe es doch schon gestartet, also ist es wahrscheinlich fertig“, aber fr Swift ist das grundlegend: Das Lesen von async let ist ein potenzieller Wartpunkt und muss mit await markiert sein. Wenn es ohne await wirklich „fast immer rechtzeitig“ fertig wurde, herzlichen Glckwunsch, Sie haben gerade Code geschrieben, der genau bis zum ersten langsamen Wi‑Fi funktioniert.
Fehler Nr. 2: Versuch, voneinander abhngige Schritte zu parallelisieren.
Wenn die zweite Anfrage ohne das Ergebnis der ersten nicht zusammengesetzt werden kann (zum Beispiel wird ein Token bentigt, ein Redirect-Link, ein nextPageToken), dann ist async let nicht geeignet. Das ist kein Swift-Problem, sondern Datenmathematik: Sie knnen Schritt 2 nicht ausfhren, bevor Sie die Eingabe fr Schritt 2 erhalten haben. In solchen Fllen ist es besser, ehrlich sequentielle awaits zu schreiben und keine Multitasking-Magie vorzutuschen.
Fehler Nr. 3: async let deklarieren und es dann „vergessen“, in der Annahme, es gehe in den Hintergrund.
async let ist kein unstructured „eigenes Leben“. Beim Verlassen des Scopes werden nicht ge-awaitete Werte implizit abgebrochen und anschlieend awaited, das heit, Sie zahlen trotzdem Zeit fr die Ausfhrung (meist fr die lngste Kindaufgabe). Wenn Sie wirklich wollen, dass etwas weiter luft, whrend Sie schon weitergehen, ist das ein anderes Werkzeug und braucht ein eigenes Gesprch und Disziplin.
Fehler Nr. 4: await ber den ganzen Code verstreuen statt an einem Sammelpunkt.
Manchmal schreibt man so: „async let erstellt, dann irgendwo in der Mitte ein Ergebnis await-et, dann am Ende das andere.“ Das ist nicht immer ein Compilerfehler, aber fast immer ein Lesbarkeitsfehler. Es ist viel einfacher, Code zu warten, wenn es einen klaren „Sammelpunkt“ gibt: let (a, b) = try await (a, b). Das macht Parallelitt aus „Magie“ zu einem klaren Schritt in der Pipeline.
Fehler Nr. 5: Versuchen, ein gemeinsames Objekt innerhalb von async let-Zweigen zu mutieren.
Selbst wenn es so scheint, als wrde man „vorsichtig“ sein, ist es in parallelen Zweigen sehr leicht, versehentlich ein gemeinsames Repository, ein gemeinsames Array oder einen gemeinsamen Logger zu berhren, sodass man einen Datenrace oder vermischte Zustnde erhlt. Swift beschrnkt solche Dinge absichtlich und behandelt sie streng. Die richtige Gewohnheit ist: Zuerst parallel Daten holen, und mutierende Operationen fr eine separate sequentielle Phase aufheben.
GO TO FULL VERSION