Pytania rekrutacyjne Swift Structured Concurrency: async/await, TaskGroup, Actors

Techniczne pytania rekrutacyjne Swift Structured Concurrency: async/await, TaskGroup, actors i wzorce współbieżności dla iOS 2026

Pytania rekrutacyjne Swift Structured Concurrency z async/await, TaskGroup i actors

Structured Concurrency wprowadzona w Swift 5.5 zrewolucjonizowała programowanie asynchroniczne na iOS. Rekruterzy testują obecnie biegłość w async/await, TaskGroup i actors podczas rozmów technicznych. Oto kluczowe pytania i oczekiwane odpowiedzi pozwalające wyróżnić się na rozmowach kwalifikacyjnych.

Kluczowe kompetencje weryfikowane na rozmowach

Rekruterzy oceniają trzy kompetencje: rozumienie podstawowych koncepcji (async/await, Task), opanowanie wzorców współbieżności (TaskGroup, izolacja actorów) oraz umiejętność diagnozowania częstych błędów (data races, deadlocki).

Jaka jest różnica między async/await a DispatchQueue?

Oczekiwana odpowiedź: async/await zapewnia structured concurrency z czytelnym kodem sekwencyjnym, podczas gdy DispatchQueue używa callbacków i może prowadzić do tzw. "callback hell". Swift automatycznie zarządza wątkami przy async/await.

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)
}

Kluczowe punkty: async/await eliminuje piramidy callbacków, redukuje błędy wątków (brak potrzeby DispatchQueue.main.async) i pozwala runtime'owi Swift optymalizować wykonanie na dostępnych rdzeniach CPU.

Przewaga wydajnościowa

Runtime Swift wykorzystuje zoptymalizowaną pulę wątków, która unika nadmiernego tworzenia wątków. W przeciwieństwie do DispatchQueue, gdzie każde .async może utworzyć nowy wątek, async/await inteligentnie wykorzystuje istniejące wątki.

Jak zarządzać wieloma operacjami asynchronicznymi równolegle?

Oczekiwana odpowiedź: Używać async let dla 2-3 prostych zadań lub TaskGroup dla dynamicznej liczby równoległych zadań ze zbieraniem wyników.

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 }
        }
    }
}

Częsty błąd: Używanie await sekwencyjnie zamiast async let zrównoleglającego wywołania. let user = await fetchUser(); let posts = await fetchPosts() wykonuje się sekwencyjnie (wolno), podczas gdy async let uruchamia oba jednocześnie.

Czym jest actor i dlaczego go używać?

Oczekiwana odpowiedź: Actor to typ chroniący swój stan mutowalny przed data races, gwarantując dostęp sekwencyjny. Zastępuje ręczne locki (NSLock, DispatchQueue) w celu zabezpieczenia dostępu współbieżnego.

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")

Kluczowe punkty: Actor gwarantuje, że tylko jeden wątek uzyskuje dostęp do jego stanu w danym momencie. Kompilator wymusza użycie await dla wywołań zewnętrznych, czyniąc potencjalne punkty zawieszenia jawnymi.

Pułapka MainActor

@MainActor to globalny actor dla operacji UI. Oznaczenie klasy @MainActor zmusza wszystkie jej metody do wykonywania na głównym wątku. Uwaga na blokujące wywołania, które mogą zamrozić interfejs.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Jak obsługiwać błędy w TaskGroup?

Oczekiwana odpowiedź: withThrowingTaskGroup propaguje pierwszy napotkany błąd i automatycznie anuluje pozostałe zadania. Aby zebrać wszystkie błędy, używać Result w TaskGroup.

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)
    }
}

Kluczowy punkt: withThrowingTaskGroup zatrzymuje się przy pierwszym błędzie (przydatne dla operacji atomowych), natomiast withTaskGroup + Result pozwala kontynuować mimo błędów (przydatne dla przetwarzania wsadowego).

Jaka jest różnica między Task, Task.detached i async let?

Oczekiwana odpowiedź: Task dziedziczy kontekst rodzica (priorytet, izolacja actorów), Task.detached tworzy niezależne zadanie bez dziedziczenia, a async let tworzy zadanie potomne automatycznie oczekiwane na końcu zakresu.

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] { [] }
}

Przypadki użycia:

  • Task: Operacje powiązane z bieżącym kontekstem (np. aktualizacja UI z ViewModel)
  • Task.detached: Niezależne zadania w tle (np. logi, analytics)
  • async let: Operacje równoległe z wynikami potrzebnymi w bieżącym zakresie

Jak zaimplementować timeout dla operacji asynchronicznej?

Oczekiwana odpowiedź: Używać Task.sleep w wyścigu między głównym zadaniem a zadaniem timeoutu z withThrowingTaskGroup, lub utworzyć narzędzie withTimeout.

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)
    }
}

Nowoczesna alternatywa: Od iOS 16 można używać URLSession z timeoutInterval skonfigurowanym przez URLSessionConfiguration specjalnie dla wywołań HTTP.

Jawne anulowanie

group.cancelAll() jest kluczowe dla zwolnienia zasobów. Bez tego przegrywające zadanie kontynuowałoby pracę w tle aż do naturalnego zakończenia, marnując CPU i pamięć.

Jak bezpiecznie współdzielić stan mutowalny między wieloma zadaniami?

Oczekiwana odpowiedź: Użyć actor dla współdzielonego stanu lub AsyncStream do komunikacji między zadaniami przez strumień wartości.

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)
}

Wybór architektoniczny: Actor dla scentralizowanego stanu z logiką biznesową, AsyncStream dla komunikacji opartej na zdarzeniach między rozłączonymi komponentami.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Czym jest anulowanie Task i jak je obsługiwać?

Oczekiwana odpowiedź: Anulowanie Task pozwala anulować trwające operacje asynchroniczne. Zadania muszą okresowo sprawdzać Task.isCancelled lub używać Task.checkCancellation(), które rzuca błąd.

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
    }
}

Kluczowe punkty:

  • Task.isCancelled: nieblokujące sprawdzenie (zwraca bool)
  • Task.checkCancellation(): rzuca CancellationError, jeśli anulowane
  • Modyfikator SwiftUI .task { }: automatyczne anulowanie przy zniknięciu widoku
Anulowanie kooperacyjne

Swift wykorzystuje model anulowania kooperacyjnego: zadania nie są wymuszanie zabijane. Kod musi aktywnie sprawdzać Task.isCancelled lub checkCancellation(), aby reagować na anulowanie. Bez tych sprawdzeń zadanie kontynuuje pracę w nieskończoność.

Jak prawidłowo używać MainActor w aplikacji SwiftUI?

Oczekiwana odpowiedź: Adnotować ViewModele atrybutem @MainActor, aby zagwarantować, że wszystkie aktualizacje stanu UI odbywają się na głównym wątku. Używać @MainActor na pojedynczych funkcjach, jeśli tylko niektóre operacje dotyczą UI.

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()
    }
}

Częsty błąd: Oznaczanie całej klasy jako @MainActor, gdy tylko niektóre metody dotyczą UI. Zmusza to cały kod do działania na głównym wątku, w tym ciężkie operacje, które powinny być w tle.

Jak obsługiwać data races za pomocą Sendable?

Oczekiwana odpowiedź: Protokół Sendable gwarantuje, że typ może być współdzielony między zadaniami bez ryzyka data race. Typy wartościowe (struct, enum) są automatycznie Sendable, klasy muszą być final z niezmiennymi lub chronionymi właściwościami.

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()
    }
}

Reguły Sendable:

  • Struct/Enum z właściwościami Sendable: automatycznie Sendable
  • Klasy: muszą być final + niezmienne, lub używać @unchecked Sendable z ręczną ochroną (locki, actory)
  • Domknięcia: automatycznie Sendable, jeśli przechwytują tylko typy Sendable
@unchecked Sendable

@unchecked Sendable wyłącza kontrole kompilatora. Używać tylko jeśli thread safety jest zagwarantowane ręcznie (locki, kolejki szeregowe). Odpowiedzialność programisty za unikanie data races.

Gotowy na rozmowy o iOS?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Podsumowanie

Opanowanie Swift Structured Concurrency stało się niezbędne dla rozmów rekrutacyjnych iOS w 2026 roku. Rekruterzy oceniają trzy poziomy: rozumienie koncepcji (async/await vs callbacki), opanowanie wzorców (TaskGroup, izolacja actorów) i debugowanie (anulowanie, Sendable).

Lista kontrolna przygotowania:

  • ✅ Wyjaśnić async/await vs DispatchQueue na konkretnym przykładzie
  • ✅ Zademonstrować użycie TaskGroup do operacji równoległych
  • ✅ Zaimplementować thread-safe actor do ochrony stanu mutowalnego
  • ✅ Obsłużyć błędy w kontekście współbieżnym (Result, throwing)
  • ✅ Rozróżnić Task, Task.detached i async let z przypadkami użycia
  • ✅ Zaimplementować timeout dla operacji asynchronicznej
  • ✅ Prawidłowo używać MainActor w architekturze SwiftUI
  • ✅ Zrozumieć Sendable i unikać data races

Najlepsi kandydaci łączą teorię z praktyką: wyjaśniają "dlaczego" (unikanie data races, poprawa czytelności) i "jak" (kod funkcjonalny z obsługą błędów). Praktyka na realnych projektach utrwala te wzorce.

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

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

Udostępnij

Powiązane artykuły