Swift Structured Concurrency Interviewfragen: async/await, TaskGroup, Actors

Technische Swift Structured Concurrency Interviewfragen: async/await, TaskGroup, Actors und Concurrency-Patterns für iOS 2026

Swift Structured Concurrency Interviewfragen mit async/await, TaskGroup und Actors

Die mit Swift 5.5 eingeführte Structured Concurrency hat die asynchrone Programmierung unter iOS revolutioniert. Recruiter testen heute in technischen Interviews die Beherrschung von async/await, TaskGroup und Actors. Hier sind die wesentlichen Fragen und erwarteten Antworten, um in Interviews zu glänzen.

Im Interview geprüfte Schlüsselkompetenzen

Recruiter bewerten drei Kompetenzen: Verständnis grundlegender Konzepte (async/await, Task), Beherrschung von Concurrency-Patterns (TaskGroup, Actor Isolation) und die Fähigkeit, häufige Fehler (Data Races, Deadlocks) zu diagnostizieren.

Was ist der Unterschied zwischen async/await und DispatchQueue?

Erwartete Antwort: async/await bietet strukturierte Concurrency mit lesbarem sequentiellem Code, während DispatchQueue Callbacks verwendet und zur sogenannten "Callback-Hölle" führen kann. Swift verwaltet die Threads mit async/await automatisch.

NetworkService.swiftswift
// Comparison: async/await vs DispatchQueue

// ❌ Old style: DispatchQueue with callbacks
func fetchUserOld(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    DispatchQueue.global().async {
        // Simulated network call
        let result = self.performNetworkRequest(id: id)
        DispatchQueue.main.async {
            completion(result)
        }
    }
}

// ✅ New style: async/await more readable
func fetchUser(id: Int) async throws -> User {
    // Swift runtime handles threads automatically
    // No need to manually switch between queues
    return try await performNetworkRequest(id: id)
}

Schlüsselpunkte: async/await eliminiert Callback-Pyramiden, reduziert Thread-Fehler (kein DispatchQueue.main.async nötig) und erlaubt der Swift-Runtime, die Ausführung über die verfügbaren CPU-Kerne zu optimieren.

Performance-Vorteil

Die Swift-Runtime nutzt einen optimierten Thread-Pool, der eine übermäßige Thread-Erzeugung vermeidet. Anders als bei DispatchQueue, wo jedes .async einen neuen Thread erzeugen kann, verwendet async/await bestehende Threads intelligent wieder.

Wie werden mehrere asynchrone Operationen parallel verwaltet?

Erwartete Antwort: async let für 2-3 einfache Tasks verwenden, oder TaskGroup für eine dynamische Anzahl paralleler Tasks mit Ergebnissammlung.

DataFetcher.swiftswift
// Parallel async operations strategies

struct DataFetcher {
    // Strategy 1: async let for fixed tasks (2-4 operations)
    func loadDashboard() async throws -> Dashboard {
        // Launch 3 requests in parallel
        async let user = fetchUser()
        async let posts = fetchPosts()
        async let notifications = fetchNotifications()

        // Wait for results (parallel, not sequential)
        let (userData, postsData, notificationsData) = try await (user, posts, notifications)

        return Dashboard(user: userData, posts: postsData, notifications: notificationsData)
    }

    // Strategy 2: TaskGroup for dynamic number of tasks
    func downloadImages(urls: [URL]) async throws -> [UIImage] {
        // TaskGroup allows managing N tasks with result collection
        try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
            // Launch one task per URL
            for (index, url) in urls.enumerated() {
                group.addTask {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    guard let image = UIImage(data: data) else {
                        throw ImageError.invalidData
                    }
                    return (index, image) // Return index to preserve order
                }
            }

            // Collect results in order
            var images = [UIImage?](repeating: nil, count: urls.count)
            for try await (index, image) in group {
                images[index] = image
            }

            return images.compactMap { $0 }
        }
    }
}

Häufiger Fehler: await sequentiell zu verwenden statt async let parallelisiert die Aufrufe. let user = await fetchUser(); let posts = await fetchPosts() läuft sequentiell (langsam), während async let beide gleichzeitig startet.

Was ist ein Actor und warum sollte man ihn verwenden?

Erwartete Antwort: Ein Actor ist ein Typ, der seinen veränderbaren Zustand vor Data Races schützt, indem er sequentiellen Zugriff garantiert. Er ersetzt manuelle Locks (NSLock, DispatchQueue) zur Absicherung paralleler Zugriffe.

CacheManager.swiftswift
// Actor for thread-safe state management

// ❌ Classic class: data race risk
class UnsafeCache {
    private var cache: [String: Data] = [:] // Not thread-safe!

    func store(_ data: Data, for key: String) {
        cache[key] = data // ⚠️ Race condition with concurrent access
    }
}

// ✅ Actor: automatic protection against races
actor SafeCache {
    private var cache: [String: Data] = [:]

    // Sequential access guaranteed by actor isolation
    func store(_ data: Data, for key: String) {
        cache[key] = data // ✅ Thread-safe automatically
    }

    func retrieve(for key: String) -> Data? {
        return cache[key] // ✅ Protected read
    }

    // Internal synchronous method (no await needed)
    nonisolated func clearAll() async {
        // nonisolated allows calling from any context
        await self.clear()
    }

    private func clear() {
        cache.removeAll()
    }
}

// Usage: await required to access actor
let cache = SafeCache()
await cache.store(data, for: "user_123") // Await mandatory
let cachedData = await cache.retrieve(for: "user_123")

Schlüsselpunkte: Der Actor stellt sicher, dass nur ein Thread gleichzeitig auf seinen Zustand zugreift. Der Compiler erzwingt die Verwendung von await für externe Aufrufe und macht potenzielle Suspension Points explizit.

MainActor-Falle

@MainActor ist ein globaler Actor für UI-Operationen. Eine Klasse mit @MainActor zu kennzeichnen, zwingt alle Methoden auf den Main Thread. Vorsicht bei blockierenden Aufrufen, die das UI einfrieren können.

Bereit für deine iOS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Wie werden Fehler in einer TaskGroup behandelt?

Erwartete Antwort: withThrowingTaskGroup propagiert den ersten aufgetretenen Fehler und bricht verbleibende Tasks automatisch ab. Um alle Fehler zu sammeln, Result in der TaskGroup verwenden.

BatchProcessor.swiftswift
// Error handling strategies in TaskGroup

struct BatchProcessor {
    // Strategy 1: First error propagation (fail-fast)
    func processItemsFastFail(items: [Item]) async throws -> [ProcessedItem] {
        try await withThrowingTaskGroup(of: ProcessedItem.self) { group in
            for item in items {
                group.addTask {
                    // If one task throws, group cancels others
                    try await self.process(item)
                }
            }

            // Collect results until first error
            var results: [ProcessedItem] = []
            for try await result in group {
                results.append(result)
            }
            return results
        }
        // ⚠️ If one task fails, others are cancelled
    }

    // Strategy 2: Collect all errors (resilience)
    func processItemsResilient(items: [Item]) async -> ([ProcessedItem], [Error]) {
        await withTaskGroup(of: Result<ProcessedItem, Error>.self) { group in
            for item in items {
                group.addTask {
                    // Wrap in Result to capture errors
                    do {
                        let result = try await self.process(item)
                        return .success(result)
                    } catch {
                        return .failure(error)
                    }
                }
            }

            // Separate successes/failures
            var successes: [ProcessedItem] = []
            var errors: [Error] = []

            for await result in group {
                switch result {
                case .success(let item):
                    successes.append(item)
                case .failure(let error):
                    errors.append(error)
                }
            }

            return (successes, errors)
        }
    }

    private func process(_ item: Item) async throws -> ProcessedItem {
        // Processing with possible error
        try await Task.sleep(nanoseconds: 100_000_000)
        return ProcessedItem(from: item)
    }
}

Schlüsselpunkt: withThrowingTaskGroup stoppt beim ersten Fehler (nützlich für atomare Operationen), während withTaskGroup + Result ermöglicht, trotz Fehlern weiterzuarbeiten (nützlich für Batch-Verarbeitung).

Was ist der Unterschied zwischen Task, Task.detached und async let?

Erwartete Antwort: Task erbt den übergeordneten Kontext (Priorität, Actor Isolation), Task.detached erstellt einen unabhängigen Task ohne Vererbung, und async let erstellt einen Child Task, der am Ende des Scopes automatisch erwartet wird.

TaskLifecycle.swiftswift
// Understanding Task creation patterns

@MainActor
class ViewModel {
    var isLoading = false

    // Scenario 1: Task inherits context (@MainActor here)
    func loadDataWithTask() {
        Task {
            // ✅ Inherits @MainActor from parent
            // No need for await MainActor.run
            self.isLoading = true
            let data = try await fetchData()
            self.isLoading = false // ✅ Always on MainActor
        }
    }

    // Scenario 2: Task.detached creates independent task
    func loadDataDetached() {
        Task.detached {
            // ⚠️ Does NOT inherit @MainActor
            let data = try await self.fetchData()

            // ❌ Error: isLoading not directly accessible
            // await MainActor.run {
            //     self.isLoading = false
            // }
        }
    }

    // Scenario 3: async let creates structured child task
    func loadMultipleData() async throws {
        // async let tasks are bound to current scope
        async let users = fetchUsers()
        async let posts = fetchPosts()

        // ⚠️ If leaving function before await, compilation error
        let (usersData, postsData) = try await (users, posts)

        // async let tasks automatically cancelled
        // if exiting scope (e.g., throw before await)
    }

    private func fetchData() async throws -> Data {
        try await URLSession.shared.data(from: URL(string: "https://api.example.com")!).0
    }

    private func fetchUsers() async throws -> [User] { [] }
    private func fetchPosts() async throws -> [Post] { [] }
}

Anwendungsfälle:

  • Task: Operationen, die an den aktuellen Kontext gebunden sind (z. B. UI-Update aus einem ViewModel)
  • Task.detached: Unabhängige Background-Tasks (z. B. Logs, Analytics)
  • async let: Parallele Operationen mit im aktuellen Scope benötigten Ergebnissen

Wie wird ein Timeout für eine asynchrone Operation implementiert?

Erwartete Antwort: Task.sleep in einem Race zwischen Haupttask und Timeout-Task mit withThrowingTaskGroup verwenden, oder ein withTimeout-Utility erstellen.

AsyncTimeout.swiftswift
// Timeout implementation for async operations

enum TimeoutError: Error {
    case timedOut
}

// Generic utility to add timeout
func withTimeout<T>(
    seconds: TimeInterval,
    operation: @escaping () async throws -> T
) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        // Task 1: main operation
        group.addTask {
            try await operation()
        }

        // Task 2: timeout
        group.addTask {
            try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
            throw TimeoutError.timedOut
        }

        // First task to finish wins
        guard let result = try await group.next() else {
            throw TimeoutError.timedOut
        }

        // Cancel losing task (important for cleanup)
        group.cancelAll()

        return result
    }
}

// Usage example
struct NetworkService {
    func fetchUserWithTimeout(id: Int) async throws -> User {
        // 5-second timeout on network call
        try await withTimeout(seconds: 5) {
            try await self.fetchUser(id: id)
        }
    }

    private func fetchUser(id: Int) async throws -> User {
        let url = URL(string: "https://api.example.com/users/\(id)")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(User.self, from: data)
    }
}

Moderne Alternative: Ab iOS 16 kann URLSession mit timeoutInterval über URLSessionConfiguration speziell für HTTP-Aufrufe konfiguriert werden.

Explizites Cancellation

group.cancelAll() ist entscheidend, um Ressourcen freizugeben. Ohne dies würde der unterlegene Task im Hintergrund bis zum natürlichen Ende weiterlaufen und CPU sowie Speicher verschwenden.

Wie wird veränderbarer Zustand sicher zwischen mehreren Tasks geteilt?

Erwartete Antwort: Einen actor für den geteilten Zustand verwenden, oder einen AsyncStream zur Kommunikation zwischen Tasks über einen Wertestrom.

SharedStateManager.swiftswift
// Safe state sharing between concurrent tasks

// Approach 1: Actor for shared state with sequential access
actor DownloadManager {
    private var activeDownloads: [String: Task<Data, Error>] = [:]
    private var cache: [String: Data] = [:]

    // Start download or return existing task
    func download(url: String) async throws -> Data {
        // Check cache first
        if let cachedData = cache[url] {
            return cachedData
        }

        // Check if download already in progress
        if let existingTask = activeDownloads[url] {
            return try await existingTask.value
        }

        // Create new download task
        let task = Task<Data, Error> {
            let data = try await self.performDownload(url: url)

            // Update cache (thread-safe via actor)
            await self.completeDownload(url: url, data: data)

            return data
        }

        activeDownloads[url] = task
        return try await task.value
    }

    private func performDownload(url: String) async throws -> Data {
        let urlObject = URL(string: url)!
        let (data, _) = try await URLSession.shared.data(from: urlObject)
        return data
    }

    private func completeDownload(url: String, data: Data) {
        cache[url] = data
        activeDownloads.removeValue(forKey: url)
    }
}

// Approach 2: AsyncStream for inter-task communication
struct EventStream {
    private let continuation: AsyncStream<Event>.Continuation
    let stream: AsyncStream<Event>

    init() {
        var continuation: AsyncStream<Event>.Continuation!
        stream = AsyncStream { cont in
            continuation = cont
        }
        self.continuation = continuation
    }

    func emit(_ event: Event) {
        continuation.yield(event)
    }

    func finish() {
        continuation.finish()
    }
}

// Example: shared progress monitoring
func processItemsWithProgress(items: [Item]) async {
    let eventStream = EventStream()

    // Task 1: Process items
    Task {
        for item in items {
            await processItem(item)
            eventStream.emit(.itemProcessed(item.id))
        }
        eventStream.finish()
    }

    // Task 2: Update UI with progress
    Task { @MainActor in
        for await event in eventStream.stream {
            switch event {
            case .itemProcessed(let id):
                print("Item \(id) processed")
            }
        }
    }
}

enum Event {
    case itemProcessed(String)
}

Architekturwahl: Actor für zentralisierten Zustand mit Geschäftslogik, AsyncStream für eventgesteuerte Kommunikation zwischen entkoppelten Komponenten.

Bereit für deine iOS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Was ist Task Cancellation und wie wird es behandelt?

Erwartete Antwort: Task Cancellation erlaubt das Abbrechen laufender asynchroner Operationen. Tasks müssen Task.isCancelled regelmäßig prüfen oder Task.checkCancellation() verwenden, das einen Fehler wirft.

CancellableOperations.swiftswift
// Implementing proper task cancellation

struct ImageProcessor {
    // Cancellable processing with explicit checks
    func processImages(_ images: [UIImage]) async throws -> [ProcessedImage] {
        var results: [ProcessedImage] = []

        for (index, image) in images.enumerated() {
            // Check 1: Boolean check (continue or skip)
            if Task.isCancelled {
                print("Cancelled after \(index) images")
                break // Graceful stop
            }

            let processed = try await processImage(image)
            results.append(processed)

            // Check 2: Automatic throw if cancelled
            try Task.checkCancellation()
        }

        return results
    }

    private func processImage(_ image: UIImage) async throws -> ProcessedImage {
        // Simulate long processing
        for _ in 0..<10 {
            try await Task.sleep(nanoseconds: 100_000_000)

            // ✅ Check cancellation in long loops
            try Task.checkCancellation()
        }

        return ProcessedImage(from: image)
    }
}

// SwiftUI: Automatic cancellation when view disappears
struct ImageGalleryView: View {
    @State private var images: [ProcessedImage] = []

    var body: some View {
        ScrollView {
            // Display images
        }
        .task {
            // ✅ Task cancelled automatically when view disappears
            let processor = ImageProcessor()
            do {
                images = try await processor.processImages(sourceImages)
            } catch is CancellationError {
                print("Processing cancelled")
            }
        }
    }
}

// Manual cancellation of stored task
class DownloadViewModel {
    private var downloadTask: Task<Void, Never>?

    func startDownload() {
        downloadTask = Task {
            do {
                try await performLongDownload()
            } catch is CancellationError {
                print("Download cancelled by user")
            }
        }
    }

    func cancelDownload() {
        // Explicit cancellation of stored task
        downloadTask?.cancel()
        downloadTask = nil
    }

    private func performLongDownload() async throws {
        try Task.checkCancellation()
        // Download logic
    }
}

Schlüsselpunkte:

  • Task.isCancelled: nicht blockierende Prüfung (gibt Bool zurück)
  • Task.checkCancellation(): wirft CancellationError, wenn abgebrochen
  • SwiftUI-Modifier .task { }: automatisches Cancellation, wenn die View verschwindet
Kooperatives Cancellation

Swift verwendet ein kooperatives Cancellation-Modell: Tasks werden nicht zwangsweise beendet. Der Code muss aktiv Task.isCancelled oder checkCancellation() prüfen, um auf Cancellation zu reagieren. Ohne diese Prüfungen läuft der Task unbegrenzt weiter.

Wie wird MainActor in einer SwiftUI-App korrekt verwendet?

Erwartete Antwort: ViewModels mit @MainActor annotieren, um sicherzustellen, dass alle UI-State-Updates auf dem Main Thread stattfinden. @MainActor an einzelnen Funktionen verwenden, wenn nur bestimmte Operationen das UI berühren.

MainActorPatterns.swiftswift
// Proper MainActor usage in SwiftUI architecture

// Pattern 1: Entire ViewModel @MainActor
@MainActor
class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var isLoading = false
    @Published var errorMessage: String?

    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    // ✅ All methods implicitly @MainActor
    func loadUser(id: Int) async {
        isLoading = true // No need for await or MainActor.run
        errorMessage = nil

        do {
            // Network call done on background thread by runtime
            user = try await repository.fetchUser(id: id)
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false // Always on MainActor
    }

    // Synchronous method also on MainActor
    func clearUser() {
        user = nil
        errorMessage = nil
    }
}

// Pattern 2: Selective methods with @MainActor
class DataSyncService {
    // ❌ Not @MainActor on class (no UI here)

    func syncData() async throws {
        // Background processing
        let data = try await fetchRemoteData()
        let processed = processData(data)

        // ✅ Switch to MainActor only for UI
        await updateUI(with: processed)
    }

    @MainActor
    private func updateUI(with data: ProcessedData) {
        // Update observable property
        NotificationCenter.default.post(
            name: .dataDidSync,
            object: data
        )
    }

    // Background work (not @MainActor)
    private func fetchRemoteData() async throws -> Data {
        // Network call
        Data()
    }

    private func processData(_ data: Data) -> ProcessedData {
        // CPU-intensive processing in background
        ProcessedData()
    }
}

// Pattern 3: Closure annotation
class ImageLoader {
    func loadImage(url: URL, completion: @MainActor @escaping (UIImage?) -> Void) async {
        let image = try? await downloadImage(from: url)

        // ✅ Completion guaranteed on MainActor
        await completion(image)
    }

    private func downloadImage(from url: URL) async throws -> UIImage {
        let (data, _) = try await URLSession.shared.data(from: url)
        return UIImage(data: data) ?? UIImage()
    }
}

Häufiger Fehler: Die gesamte Klasse als @MainActor zu kennzeichnen, wenn nur bestimmte Methoden das UI betreffen. Das zwingt allen Code auf den Main Thread, einschließlich rechenintensiver Operationen, die in den Hintergrund gehören.

Wie werden Data Races mit Sendable behandelt?

Erwartete Antwort: Das Sendable-Protokoll garantiert, dass ein Typ ohne Data-Race-Risiko zwischen Tasks geteilt werden kann. Value Types (struct, enum) sind automatisch Sendable, Klassen müssen final mit unveränderlichen oder geschützten Properties sein.

SendableCompliance.swiftswift
// Making types safe for concurrent access

// ✅ Struct: automatically Sendable (value type)
struct UserData: Sendable {
    let id: Int
    let name: String
    let email: String
}

// ✅ Enum: automatically Sendable
enum LoadingState: Sendable {
    case idle
    case loading
    case loaded(UserData)
    case failed(Error) // ⚠️ Error must also be Sendable
}

// ❌ Class with mutable state: not Sendable by default
class UnsafeCounter {
    var count = 0 // Mutable, unprotected

    func increment() {
        count += 1 // Data race possible
    }
}

// ✅ Immutable class: explicit Sendable
final class SafeConfig: @unchecked Sendable {
    let apiKey: String
    let timeout: TimeInterval

    init(apiKey: String, timeout: TimeInterval) {
        self.apiKey = apiKey
        self.timeout = timeout
    }
}

// ✅ Class with actor-protected state
actor SafeCounter: Sendable {
    private var count = 0 // Protected by actor isolation

    func increment() {
        count += 1 // Thread-safe automatically
    }

    func getValue() -> Int {
        return count
    }
}

// ✅ Class with manually protected state
final class ThreadSafeCache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Data] = [:]

    func store(_ data: Data, for key: String) {
        lock.lock()
        defer { lock.unlock() }
        storage[key] = data
    }

    func retrieve(for key: String) -> Data? {
        lock.lock()
        defer { lock.unlock() }
        return storage[key]
    }
}

// Usage: compiler checks Sendable
func processInBackground(data: UserData) { // ✅ UserData is Sendable
    Task.detached {
        // No warning: UserData is Sendable value type
        print("Processing user: \(data.name)")
    }
}

func processUnsafe(counter: UnsafeCounter) {
    Task.detached {
        // ⚠️ Warning: UnsafeCounter is not Sendable
        // counter.increment()
    }
}

Sendable-Regeln:

  • Struct/Enum mit Sendable-Properties: automatisch Sendable
  • Klassen: müssen final + unveränderlich sein, oder @unchecked Sendable mit manueller Absicherung (Locks, Actors) verwenden
  • Closures: automatisch Sendable, wenn nur Sendable-Typen erfasst werden
@unchecked Sendable

@unchecked Sendable deaktiviert die Compiler-Prüfungen. Nur verwenden, wenn Thread Safety manuell garantiert ist (Locks, Serial Queues). Es liegt in der Verantwortung des Entwicklers, Data Races zu vermeiden.

Bereit für deine iOS-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Fazit

Die Beherrschung von Swift Structured Concurrency ist für iOS-Interviews 2026 unverzichtbar geworden. Recruiter prüfen drei Ebenen: Verständnis der Konzepte (async/await vs. Callbacks), Beherrschung der Patterns (TaskGroup, Actor Isolation) und Debugging (Cancellation, Sendable).

Vorbereitungs-Checkliste:

  • ✅ async/await vs. DispatchQueue mit konkretem Beispiel erklären
  • ✅ TaskGroup-Verwendung für parallele Operationen demonstrieren
  • ✅ Thread-sicheren Actor zum Schutz veränderbaren Zustands implementieren
  • ✅ Fehler in nebenläufigem Kontext behandeln (Result, throwing)
  • ✅ Task, Task.detached und async let mit Anwendungsfällen unterscheiden
  • ✅ Timeout für eine asynchrone Operation implementieren
  • ✅ MainActor in einer SwiftUI-Architektur korrekt verwenden
  • ✅ Sendable verstehen und Data Races vermeiden

Die besten Kandidaten kombinieren Theorie und Praxis: Sie erklären das "Warum" (Data Races vermeiden, Lesbarkeit verbessern) und das "Wie" (funktionsfähiger Code mit Fehlerbehandlung). Für das Verfestigen dieser Patterns lohnt sich die Übung an realen Projekten.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#swift
#concurrency
#async-await
#actors
#interview

Teilen

Verwandte Artikel