Swiftlang
Art
Lektion 10 von 12
Niveau
Fortgeschritten
Lesezeit
etwa 5 Minuten
Stand

async/await in Swift

Eine Funktion, die warten muss, ist mit async markiert, und jeder Aufruf steht hinter await. An dieser Stelle darf das Programm pausieren, ohne einen Thread zu blockieren. Parallel arbeitest du mit async let oder einer Task-Gruppe. Abbrechen ist kooperativ: Der Code prüft selbst, ob er aufhören soll. Actors schützen veränderlichen Zustand, und Swift 6 erkennt viele Datenrennen schon beim Übersetzen.

Danach kannst du

  • schreibst und rufst async-Funktionen mit await auf
  • startest Arbeit parallel mit async let und withTaskGroup
  • brichst Tasks ab und reagierst korrekt darauf
  • schützt gemeinsamen Zustand mit einem Actor
  • liest die Datenrennen-Meldung des Swift-6-Compilers

Umgebung

Swift 6.4.0, Sprachmodus 6 (vollständige Concurrency-Prüfung)
plattformunabhängig, geprüft unter Linux x86-64

Die Beispiele sind Skripte mit Code auf oberster Ebene. In Apps startet dieser Code meist in einer async-Funktion oder einem Task.

async und await

Manche Arbeit dauert, weil auf etwas gewartet wird: auf eine Antwort aus dem Netz, auf eine Datei, auf einen Timer. Früher hat man dafür Callbacks verschachtelt. Seit Swift 5.5 schreibst du solchen Code von oben nach unten, wie normalen Code (SE-0296):

grundlagen.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
func ladeProfil(id: Int) async -> String {
    // Simuliert eine langsame Quelle, zum Beispiel ein Netzwerk
    try? await Task.sleep(for: .milliseconds(100))
    return "Profil \(id)"
}

func ladeBeitraege(fuer id: Int) async -> [String] {
    try? await Task.sleep(for: .milliseconds(150))
    return ["Beitrag A von \(id)", "Beitrag B von \(id)"]
}

// Nacheinander: die zweite Anfrage startet erst nach der ersten
let start = ContinuousClock.now
let profil = await ladeProfil(id: 7)
let beitraege = await ladeBeitraege(fuer: 7)
let nacheinander = ContinuousClock.now - start
print(profil, beitraege.count)

// Gleichzeitig mit async let: beide laufen parallel
let start2 = ContinuousClock.now
async let p = ladeProfil(id: 8)
async let b = ladeBeitraege(fuer: 8)
let (profil2, beitraege2) = await (p, b)
let parallel = ContinuousClock.now - start2
print(profil2, beitraege2.count)

print("parallel schneller:", parallel < nacheinander)

Ausgabe

Profil 7 2
Profil 8 2
parallel schneller: true

async in der Signatur heißt: Diese Funktion darf pausieren. await beim Aufruf markiert die Stelle, an der das passieren kann. Während die Funktion wartet, ist der Thread frei für andere Arbeit. Wie try bei Fehlern ist await ein Hinweis für Leser: Hier kann Zeit vergehen, und danach kann sich die Welt verändert haben.

Task.sleep(for:) simuliert hier eine langsame Quelle. Es kann einen Fehler werfen, wenn die Arbeit abgebrochen wird, deshalb steht try? davor. Mehr dazu weiter unten.

Nacheinander oder gleichzeitig

Die ersten beiden Aufrufe laufen nacheinander: Das Programm wartet etwa 100 Millisekunden auf das Profil und erst dann 150 auf die Beiträge. Hängen die beiden nicht voneinander ab, ist das verschenkte Zeit.

Mit async let startest du beide sofort und wartest erst später gemeinsam auf die Ergebnisse. Die Gesamtzeit ist dann ungefähr die der langsameren Aufgabe. Die letzte Zeile der Ausgabe bestätigt, dass die parallele Variante schneller war. Die genauen Zeiten schwanken von Lauf zu Lauf, deshalb gibt das Beispiel nur den Vergleich aus.

Task-Gruppen

async let passt, wenn du die Anzahl der Aufgaben beim Schreiben kennst. Für eine Liste unbekannter Länge gibt es Task-Gruppen:

taskgroup.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
func pruefsumme(von datei: String) async -> Int {
    try? await Task.sleep(for: .milliseconds(datei.count * 10))
    return datei.unicodeScalars.reduce(0) { $0 + Int($1.value) }
}

let dateien = ["a.swift", "Package.swift", "README.md", "main.swift"]

// Eine Task-Gruppe startet beliebig viele Kind-Tasks und sammelt die Ergebnisse ein
let ergebnisse = await withTaskGroup(of: (String, Int).self) { gruppe in
    for datei in dateien {
        gruppe.addTask { (datei, await pruefsumme(von: datei)) }
    }
    var gesammelt: [String: Int] = [:]
    for await (datei, summe) in gruppe {
        gesammelt[datei] = summe
    }
    return gesammelt
}

// Ergebnisse kommen in beliebiger Reihenfolge an, daher in fester Reihenfolge ausgeben
for datei in dateien {
    print(datei, ergebnisse[datei]!)
}

Ausgabe

a.swift 700
Package.swift 1287
README.md 685
main.swift 1024

withTaskGroup startet für jede Datei eine Kind-Task. Mit for await sammelst du die Ergebnisse ein, in der Reihenfolge, in der sie fertig werden, nicht in der, in der du sie gestartet hast. Deshalb landen sie im Dictionary und werden in der Reihenfolge der Eingabe ausgegeben.

Das ist strukturierte Nebenläufigkeit (SE-0304): Keine Kind-Task überlebt ihre Gruppe. Wenn withTaskGroup zurückkehrt, sind alle fertig. Wird die umgebende Task abgebrochen, werden auch alle Kinder abgebrochen. Du kannst keine Arbeit „vergessen“, die im Hintergrund weiterläuft.

Abbrechen

Abbrechen ist in Swift kooperativ: cancel() setzt nur eine Markierung. Die Task selbst entscheidet, wann sie darauf reagiert.

abbrechen.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
func verarbeite(_ anzahl: Int) async throws -> Int {
    var erledigt = 0
    for _ in 1...anzahl {
        try Task.checkCancellation()          // wirft CancellationError, wenn abgebrochen wurde
        try await Task.sleep(for: .milliseconds(20))
        erledigt += 1
    }
    return erledigt
}

let aufgabe = Task {
    try await verarbeite(1_000)
}

try? await Task.sleep(for: .milliseconds(100))
aufgabe.cancel()

do {
    let n = try await aufgabe.value
    print("fertig:", n)
} catch is CancellationError {
    print("abgebrochen, bevor alle 1000 Schritte erledigt waren")
}

Ausgabe

abgebrochen, bevor alle 1000 Schritte erledigt waren

Task.checkCancellation() wirft einen CancellationError, sobald die Task abgebrochen wurde. Auch Task.sleep wirft sofort, wenn die Task während des Schlafens abgebrochen wird. Die Schleife endet also nach wenigen Durchläufen statt nach 1000. Der Fehler wandert über try await aufgabe.value nach außen und wird dort gezielt mit catch is CancellationError behandelt.

Prüfe auf Abbruch vor jedem teuren Schritt. Code, der nie prüft, läuft nach cancel() einfach weiter bis zum Ende. Wenn du statt eines Fehlers lieber einen Teilwert zurückgibst, fragst du Task.isCancelled ab.

Actors: Zustand schützen

Sobald mehrere Tasks gleichzeitig denselben veränderlichen Zustand ändern, droht ein Datenrennen: Zwei Zugriffe überschneiden sich, und das Ergebnis hängt vom Zufall ab. Swift 6 lehnt solchen Code beim Übersetzen ab:

datenrennen.swiftKompiliert absichtlich nicht Meldung von Swift 6.4.0, Linux x86-64, 19.10.2026
final class Zaehler {
    var stand = 0
}

func zaehleParallel() async {
    let zaehler = Zaehler()
    await withTaskGroup(of: Void.self) { gruppe in
        for _ in 1...10 {
            gruppe.addTask { zaehler.stand += 1 }
        }
    }
    print(zaehler.stand)
}

Compiler-Meldung

datenrennen.swift:9:20: error: passing closure as a 'sending' parameter risks causing data races between code in the current isolation context and concurrent execution of the closure [#RegionIsolation::SendingClosureRisksDataRace]
 7 |     await withTaskGroup(of: Void.self) { gruppe in
 8 |         for _ in 1...10 {
 9 |             gruppe.addTask { zaehler.stand += 1 }
   |                    |         `- note: closure captures 'zaehler' which is accessible to code in the current isolation context
   |                    `- error: passing closure as a 'sending' parameter risks causing data races between code in the current isolation context and concurrent execution of the closure [#RegionIsolation::SendingClosureRisksDataRace]
10 |         }
11 |     }

[#RegionIsolation]: <https://docs.swift.org/compiler/documentation/diagnostics/region-isolation>
[#SendingClosureRisksDataRace]: <https://docs.swift.org/compiler/documentation/diagnostics/sending-closure-risks-data-race>

Die Meldung ist lang, sagt aber genau das: Das Closure, das parallel laufen soll, fängt zaehler ein, ein Objekt, das auch der aufrufende Code sieht. Zehn Tasks könnten stand += 1 gleichzeitig ausführen. Am Ende der Meldung verlinkt der Compiler eine Erklärung auf swift.org.

Die Lösung ist ein Actor (SE-0306). Ein Actor ist wie eine Klasse, lässt aber immer nur eine Task zur Zeit an seinen Zustand:

actor.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
actor Konto {
    private(set) var stand = 0

    func einzahlen(_ betrag: Int) {
        stand += betrag
    }
}

let konto = Konto()

// 1000 gleichzeitige Einzahlungen: der Actor lässt immer nur eine zur Zeit an seinen Zustand
await withTaskGroup(of: Void.self) { gruppe in
    for _ in 1...1_000 {
        gruppe.addTask { await konto.einzahlen(1) }
    }
}

print(await konto.stand)

Ausgabe

1000

Tausend gleichzeitige Einzahlungen, und das Ergebnis ist zuverlässig 1000. Von außen ist jeder Zugriff auf einen Actor async, deshalb steht await vor konto.einzahlen(1) und vor konto.stand: Wenn der Actor gerade beschäftigt ist, wartet der Aufrufer, ohne zu blockieren.

Ein Struct hätte das Problem gar nicht erst: Werttypen werden kopiert statt geteilt (siehe Structs, Klassen und Enums). Typen, die sich sicher zwischen Tasks weitergeben lassen, erfüllen das Protokoll Sendable; für viele Structs leitet der Compiler das selbst ab.

Was das für Apps bedeutet

In Apps läuft die Oberfläche auf dem Main Actor, einem besonderen Actor für alles, was den Bildschirm verändert. SwiftUI-Views sind ihm zugeordnet. Lange Arbeit startest du mit await von dort aus, und das Ergebnis kommt automatisch wieder auf dem Main Actor an. Wie Xcode neue Projekte dafür voreinstellt, hat sich mit den letzten Swift-Versionen geändert; den aktuellen Stand und die Einstellungen beschreibt der Migrationsleitfaden auf swift.org.

Häufige Fehler

expression is 'async' but is not marked with 'await'. Vor einem async-Aufruf fehlt await:

await-fehlt.swiftKompiliert absichtlich nicht Meldung von Swift 6.4.0, Linux x86-64, 19.10.2026
func ladeZahl() async -> Int { 42 }

func zeige() async {
    let zahl = ladeZahl()
    print(zahl)
}

Compiler-Meldung

await-fehlt.swift:4:16: error: expression is 'async' but is not marked with 'await'
2 | 
3 | func zeige() async {
4 |     let zahl = ladeZahl()
  |                |- error: expression is 'async' but is not marked with 'await'
  |                `- note: call is 'async'
5 |     print(zahl)
6 | }

'async' call in a function that does not support concurrency. Du rufst async-Code aus einer normalen Funktion auf. Mach sie async oder starte einen Task { }.

passing closure as a 'sending' parameter risks causing data races …. Ein paralleles Closure teilt veränderlichen Zustand. Nutze einen Actor, einen Werttyp oder gib das Ergebnis aus der Task zurück, statt es von innen zu schreiben.

Übungen

  1. Schreibe func quadratLangsam(_ n: Int) async -> Int, die 50 Millisekunden schläft und n * n liefert. Berechne mit einer Task-Gruppe die Quadrate von 1 bis 10 und gib ihre Summe aus.
  2. Ändere das Konto-Beispiel: Ergänze eine Methode abheben(_ betrag: Int) throws -> Int, die bei zu geringem Stand einen Fehler wirft. Führe 10 Einzahlungen und danach eine zu große Abhebung aus und gib Stand und Fehler aus.
Lösung zu Übung 1
loesung-1.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
func quadratLangsam(_ n: Int) async -> Int {
    try? await Task.sleep(for: .milliseconds(50))
    return n * n
}

let summe = await withTaskGroup(of: Int.self) { gruppe in
    for n in 1...10 {
        gruppe.addTask { await quadratLangsam(n) }
    }
    var gesamt = 0
    for await quadrat in gruppe {
        gesamt += quadrat
    }
    return gesamt
}
print(summe)

Ausgabe

385

Die Summe hängt nicht von der Reihenfolge ab, deshalb kannst du die Ergebnisse direkt aufaddieren, sobald sie ankommen.

Lösung zu Übung 2
loesung-2.swiftAusgeführt Swift 6.4.0, Linux x86-64, 19.10.2026
enum KontoFehler: Error {
    case nichtGenugGuthaben(verfuegbar: Int)
}

actor Konto {
    private(set) var stand = 0

    func einzahlen(_ betrag: Int) {
        stand += betrag
    }

    func abheben(_ betrag: Int) throws -> Int {
        guard betrag <= stand else {
            throw KontoFehler.nichtGenugGuthaben(verfuegbar: stand)
        }
        stand -= betrag
        return betrag
    }
}

let konto = Konto()
for _ in 1...10 {
    await konto.einzahlen(5)
}

do {
    _ = try await konto.abheben(80)
} catch KontoFehler.nichtGenugGuthaben(let verfuegbar) {
    print("Abgelehnt: nur \(verfuegbar) verfügbar")
}
print("Stand:", await konto.stand)

Ausgabe

Abgelehnt: nur 50 verfügbar
Stand: 50

Aufrufe von außen brauchen hier try await: await, weil es ein Actor ist, try, weil die Methode werfen kann.