Swift Concurrency: async/await, Tasks und Actors in der Praxis

Nebenläufigkeit war in Swift lange ein Minenfeld aus Completion-Handlern, verschachtelten Closures und schwer auffindbaren Race Conditions. Mit Swift Concurrency – eingeführt mit Swift 5.5 – hat Apple ein modernes, sprachintegriertes Modell geschaffen, das asynchronen Code so lesbar macht wie synchronen. Dieser Artikel führt dich durch die wichtigsten Bausteine: async/await, Tasks, parallele Ausführung und Actors zum Schutz vor Datenrennen.

Das Problem mit Completion-Handlern

Früher sah asynchroner Code in iOS oft so aus: Du startest einen Netzwerkaufruf und übergibst einen Closure, der irgendwann mit dem Ergebnis aufgerufen wird. Bei mehreren aufeinanderfolgenden Aufrufen entsteht daraus die berüchtigte „Pyramid of Doom“ – tief verschachtelte Closures, in denen Fehlerbehandlung leicht vergessen wird und der Kontrollfluss kaum noch nachvollziehbar ist. Zudem ist es einfach, vergessen, den Completion-Handler in einem Fehlerpfad aufzurufen, was zu hängenden Operationen führt.

async/await: linearer Code für asynchrone Aufgaben

Mit async/await markierst du eine Funktion, die asynchron arbeitet, mit dem Schlüsselwort async. Beim Aufruf schreibst du await davor. An dieser Stelle darf die Funktion pausieren (man spricht von einem „suspension point“), gibt den Thread frei und macht später weiter, wenn das Ergebnis da ist. Der entscheidende Vorteil: Der Code liest sich von oben nach unten wie ganz normaler synchroner Code, obwohl im Hintergrund pausiert und fortgesetzt wird.

Tasks: die Brücke vom synchronen in den asynchronen Kontext

Wenn du aus einer ganz normalen, synchronen Stelle heraus etwas Asynchrones starten willst – etwa wenn ein Button gedrückt wird – brauchst du einen Task. Ein Task ist eine Einheit nebenläufiger Arbeit. In SwiftUI ist der Modifier .task der saubere Standardweg: Er startet asynchrone Arbeit beim Erscheinen einer View und bricht sie automatisch ab, wenn die View verschwindet.

Ein wichtiges Konzept ist strukturierte Nebenläufigkeit: Tasks bilden eine Hierarchie. Bricht ein Eltern-Task ab, werden auch seine Kind-Tasks abgebrochen. Das verhindert verwaiste Operationen, die im Hintergrund weiterlaufen, obwohl niemand mehr auf ihr Ergebnis wartet. Über Task.isCancelled oder try Task.checkCancellation() kannst du an passenden Stellen prüfen, ob die Arbeit noch gebraucht wird – wichtig für lange Schleifen oder Downloads.

Parallelität: async let und TaskGroup

Bisher lief alles nacheinander. Oft willst du aber mehrere unabhängige Aufgaben gleichzeitig starten und erst am Ende auf alle warten – etwa drei Bilder parallel laden statt eines nach dem anderen.

async let für eine feste Anzahl Aufgaben

Mit async let startest du eine asynchrone Operation, ohne sofort darauf zu warten. Die Arbeit beginnt im Hintergrund, und erst dort, wo du das Ergebnis mit await tatsächlich brauchst, hältst du an. Startest du drei Downloads mit drei async let hintereinander, laufen sie parallel; die Gesamtzeit entspricht etwa der des langsamsten statt der Summe aller. Das ist ideal, wenn die Zahl der Aufgaben zur Compile-Zeit feststeht.

TaskGroup für dynamisch viele Aufgaben

Weißt du erst zur Laufzeit, wie viele Teilaufgaben es gibt – etwa „lade alle Bilder aus dieser Liste“ – nutzt du eine TaskGroup. Du fügst der Gruppe in einer Schleife beliebig viele Kind-Tasks hinzu und sammelst die Ergebnisse ein, sobald sie eintreffen. Auch hier gilt strukturierte Nebenläufigkeit: Die Gruppe wartet, bis alle Kinder fertig sind, und ein Fehler in einem Kind-Task kann die ganze Gruppe sauber abbrechen.

Actors: Schutz vor Data Races

Sobald mehrere Tasks parallel auf denselben veränderbaren Zustand zugreifen, droht ein Data Race: zwei Threads schreiben gleichzeitig, das Ergebnis ist undefiniert, und solche Bugs sind notorisch schwer zu reproduzieren. Klassisch löste man das mit Locks oder seriellen Dispatch-Queues – fehleranfällig und leicht zu vergessen.

Ein Actor ist Swifts eingebaute Antwort darauf. Ein Actor sieht aus wie eine Klasse, garantiert aber, dass immer nur ein Task zur Zeit auf seinen veränderbaren Zustand zugreift. Der Compiler erzwingt das: Zugriffe von außen erfolgen automatisch über await, weil sie eventuell warten müssen, bis der Actor frei ist. Damit sind ganze Klassen von Race Conditions schon zur Compile-Zeit ausgeschlossen – nicht erst durch Glück im Testlauf.

@MainActor: alles auf dem Hauptthread

UI-Updates müssen in iOS auf dem Hauptthread passieren. Der globale @MainActor stellt genau das sicher. Markierst du eine Klasse, Methode oder Property mit @MainActor, garantiert Swift, dass der Code auf dem Hauptthread läuft. In Verbindung mit SwiftUI-Modellen ist das der saubere Weg, um „Background-Thread aktualisiert die UI“-Abstürze zu vermeiden. Netzwerkarbeit läuft im Hintergrund, das Ergebnis landet über @MainActor sicher wieder im UI.

Sendable und das strikte Concurrency-Modell

Damit der Compiler überhaupt prüfen kann, ob Daten gefahrlos zwischen Tasks weitergereicht werden dürfen, gibt es das Protokoll Sendable. Ein sendbarer Typ kann sicher über Concurrency-Grenzen geschickt werden – Wertetypen wie Structs mit sendbaren Eigenschaften erfüllen das meist automatisch. Mit dem strikten Concurrency-Checking, das mit Swift 6 zum Standard wird, weist dich der Compiler aktiv auf potenziell unsichere Übergaben hin. Das wirkt anfangs streng, eliminiert aber eine ganze Fehlerkategorie, bevor die App überhaupt läuft.

Typische Fehler und wie du sie vermeidest

Wann lohnt der Umstieg?

Wenn du eine neue App oder ein neues Feature beginnst, gibt es kaum einen Grund, noch Completion-Handler zu schreiben. Die meisten System-APIs bieten inzwischen async-Varianten an – Netzwerkaufrufe, Dateioperationen, viele Frameworks. Bei Bestandscode kannst du schrittweise migrieren: Über sogenannte Continuations lässt sich eine alte Callback-API in eine async-Funktion einpacken, sodass du nicht alles auf einmal umstellen musst.

Wer tiefer in die Sprache einsteigen möchte, findet in unserer Einführung zu SwiftUI-Grundlagen und im Artikel zu SwiftUI State Management gute Anschlusspunkte – gerade die Kombination aus @MainActor und @Observable-Modellen begegnet dir in jeder modernen App. Für schnelle Hilfsmittel beim Entwickeln, etwa zum Formatieren von JSON-Antworten oder zum Generieren von Testdaten, lohnt ein Blick in den Webtools-Bereich.

Häufige Fragen

Ab welcher iOS-Version ist Swift Concurrency verfügbar?

Async/await, Tasks und Actors stehen ab iOS 13 zur Verfügung, wenn du mit einem aktuellen Xcode baust (Apple hat die Laufzeitunterstützung zurückportiert). Für neue Projekte gibt es daher praktisch keine Hürde mehr, das moderne Modell zu nutzen.

Was ist der Unterschied zwischen async let und TaskGroup?

async let eignet sich für eine feste, kleine Anzahl paralleler Aufgaben, die zur Compile-Zeit feststeht. Eine TaskGroup nutzt du, wenn die Anzahl der Teilaufgaben erst zur Laufzeit bekannt ist, etwa beim Verarbeiten einer dynamischen Liste.

Brauche ich noch GCD und DispatchQueue?

Für neuen Code in der Regel nicht. Swift Concurrency ersetzt die meisten Anwendungsfälle von Grand Central Dispatch auf eine sicherere, lesbarere Weise. GCD bleibt nur für sehr spezielle Low-Level-Szenarien relevant.

Warum verlangt der Compiler plötzlich await an einer Stelle?

Das passiert, wenn du auf einen Actor oder eine async-Funktion zugreifst. Der Zugriff könnte warten müssen, bis der Actor frei ist oder das Ergebnis vorliegt – deshalb erzwingt der Compiler ein await, um diese mögliche Pause sichtbar zu machen.