Parallelität und Multithreading in Swift: Fortgeschrittenes Niveau
\n\nModerne Anwendungen für iOS und macOS erfordern hohe Leistung und Reaktionsfähigkeit der Benutzeroberfläche. Die fortgeschrittene Arbeit mit Parallelität in Swift geht weit über die einfache Nutzung von Grand Central Dispatch (GCD) hinaus. In diesem Artikel betrachten wir die wichtigsten Werkzeuge von Swift 5.5+ zur Verwaltung asynchroner Aufgaben: async/await, Actors, TaskGroup und das Protokoll Sendable. Sie erfahren, wie Sie Datenrennen vermeiden, Multithreading-Code richtig strukturieren und die Leistung optimieren.
1. Grundlagen von async/await und Structured Concurrency
\n\nDie Schlüsselwörter async und await sind die Grundlage moderner Parallelität in Swift. Sie ermöglichen es, asynchronen Code zu schreiben, der wie synchroner Code aussieht, was das Lesen und Debuggen erheblich vereinfacht.
Deklaration einer asynchronen Funktion:
\nfunc fetchUserData() async throws -> User {\n let url = URL(string: "https://api.example.com/user")!\n let (data, _) = try await URLSession.shared.data(from: url)\n let decoder = JSONDecoder()\n return try decoder.decode(User.self, from: data)\n}\n\nAufruf einer asynchronen Funktion mit Fehlerbehandlung:
\nTask {\n do {\n let user = try await fetchUserData()\n print("User name: \\(user.name)")\n } catch {\n print("Failed to fetch user: \\(error)")\n }\n}\n\nWichtige Punkte:
\n- \n
- Structured Concurrency: Jede asynchrone Aufgabe hat eine klare Lebensdauer, die durch den Gültigkeitsbereich des übergeordneten
Taskbegrenzt ist. \n - Abbrechen von Aufgaben: Automatisches Abbrechen von untergeordneten Aufgaben beim Abbrechen des übergeordneten Tasks. \n
- Prioritäten: Verwenden Sie
Task(priority: .userInitiated)für Aufgaben, die für den Benutzer wichtig sind. \n
2. Actors: Schutz vor Datenrennen
\n\nVor Swift 5.5 verwendeten Entwickler oft serialisierte GCD-Warteschlangen, um gemeinsamen Zustand zu schützen. Actors bieten eine eingebaute Isolation: Der Zugriff auf den Zustand eines Actors ist nur über await möglich, was die Threadsicherheit auf Compiler-Ebene garantiert.
Beispiel eines Actors für ein Bankkonto:
\nactor BankAccount {\n private var balance: Double = 0.0\n\n func deposit(amount: Double) {\n balance += amount\n }\n\n func withdraw(amount: Double) throws {\n guard balance >= amount else {\n throw BankError.insufficientFunds\n }\n balance -= amount\n }\n\n func getBalance() -> Double {\n return balance\n }\n}\n\n// Verwendung\nlet account = BankAccount()\nTask {\n await account.deposit(amount: 100.0)\n let currentBalance = await account.getBalance()\n print("Balance: \\(currentBalance)")\n}\n\nVorteile von Actors:
\n- \n
- Compiler-Prüfung: Der Versuch, auf eine Eigenschaft eines Actors ohne
awaitzuzugreifen, führt zu einem Compiler-Fehler. \n - Wiedereintritt (Reentrancy): Ein Actor kann die Ausführung der aktuellen Methode anhalten (z. B. bei
awaitinnerhalb des Actors) und einen anderen Aufruf bearbeiten, was Deadlocks verhindert. \n - Leistung: Actors verwenden optimierte Synchronisationsmechanismen, die oft schneller sind als manuelle Sperren. \n
3. TaskGroup: Parallele Datenverarbeitung
\n\nWenn mehrere unabhängige asynchrone Operationen ausgeführt und auf deren Abschluss gewartet werden müssen, ist TaskGroup das ideale Werkzeug. Es verwaltet automatisch die Anzahl der parallelen Aufgaben und sammelt die Ergebnisse.
Beispiel für das parallele Herunterladen mehrerer Bilder:
\nfunc downloadImages(urls: [URL])