Perguntas de entrevista Swift Structured Concurrency: async/await, TaskGroup, Actors

Perguntas técnicas de entrevista Swift Structured Concurrency: async/await, TaskGroup, actors e padrões de concorrência para iOS 2026

Perguntas de entrevista Swift Structured Concurrency com async/await, TaskGroup e actors

A Structured Concurrency introduzida no Swift 5.5 revolucionou a programação assíncrona no iOS. Os recrutadores agora avaliam o domínio de async/await, TaskGroup e actors durante as entrevistas técnicas. Aqui estão as perguntas essenciais e as respostas esperadas para se destacar nas entrevistas.

Habilidades-chave avaliadas em entrevistas

Os recrutadores avaliam três competências: compreensão dos conceitos fundamentais (async/await, Task), domínio dos padrões de concorrência (TaskGroup, isolamento de actor) e capacidade de diagnosticar erros comuns (data races, deadlocks).

Qual é a diferença entre async/await e DispatchQueue?

Resposta esperada: async/await fornece concorrência estruturada com código sequencial legível, enquanto DispatchQueue usa callbacks e pode levar ao chamado "callback hell". O Swift gerencia threads automaticamente com 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)
}

Pontos-chave: async/await elimina as pirâmides de callbacks, reduz erros de threads (sem necessidade de DispatchQueue.main.async) e permite que o runtime do Swift otimize a execução nos núcleos de CPU disponíveis.

Vantagem de desempenho

O runtime do Swift usa um thread pool otimizado que evita a criação excessiva de threads. Ao contrário do DispatchQueue, onde cada .async pode criar uma nova thread, o async/await reutiliza threads existentes de forma inteligente.

Como gerenciar várias operações assíncronas em paralelo?

Resposta esperada: Usar async let para 2-3 tarefas simples, ou TaskGroup para um número dinâmico de tarefas paralelas com coleta de resultados.

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

Erro comum: Usar await sequencialmente em vez de async let paraleliza as chamadas. let user = await fetchUser(); let posts = await fetchPosts() executa sequencialmente (lento), enquanto async let lança ambos simultaneamente.

O que é um actor e por que usá-lo?

Resposta esperada: Um actor é um tipo que protege seu estado mutável contra data races garantindo acesso sequencial. Ele substitui locks manuais (NSLock, DispatchQueue) para proteger o acesso concorrente.

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

Pontos-chave: O actor garante que apenas uma thread acesse seu estado por vez. O compilador exige o uso de await para chamadas externas, tornando explícitos os pontos de suspensão potenciais.

Armadilha do MainActor

@MainActor é um actor global para operações de UI. Marcar uma classe @MainActor força todos os seus métodos a executarem na thread principal. Cuidado com chamadas bloqueantes que podem congelar a interface.

Pronto para mandar bem nas entrevistas de iOS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Como gerenciar erros em um TaskGroup?

Resposta esperada: withThrowingTaskGroup propaga o primeiro erro encontrado e cancela automaticamente as tarefas restantes. Para coletar todos os erros, usar Result no 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)
    }
}

Ponto-chave: withThrowingTaskGroup para no primeiro erro (útil para operações atômicas), enquanto withTaskGroup + Result permite continuar apesar dos erros (útil para processamento em lote).

Qual é a diferença entre Task, Task.detached e async let?

Resposta esperada: Task herda o contexto pai (prioridade, isolamento de actor), Task.detached cria uma tarefa independente sem herança, e async let cria uma tarefa filha que aguarda automaticamente no final do escopo.

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

Casos de uso:

  • Task: Operações ligadas ao contexto atual (por exemplo, atualização de UI a partir de um ViewModel)
  • Task.detached: Tarefas independentes em background (por exemplo, logs, analytics)
  • async let: Operações paralelas com resultados necessários no escopo atual

Como implementar um timeout em uma operação assíncrona?

Resposta esperada: Usar Task.sleep em uma corrida entre a tarefa principal e uma tarefa de timeout com withThrowingTaskGroup, ou criar um utilitário 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)
    }
}

Alternativa moderna: A partir do iOS 16, usar URLSession com timeoutInterval configurado via URLSessionConfiguration especificamente para chamadas HTTP.

Cancelamento explícito

group.cancelAll() é crucial para liberar recursos. Sem isso, a tarefa perdedora continuaria em background até a conclusão natural, desperdiçando CPU e memória.

Como compartilhar estado mutável de forma segura entre várias tarefas?

Resposta esperada: Usar um actor para o estado compartilhado, ou um AsyncStream para se comunicar entre tarefas via fluxo de valores.

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

Escolha arquitetural: Actor para estado centralizado com lógica de negócios, AsyncStream para comunicação orientada a eventos entre componentes desacoplados.

Pronto para mandar bem nas entrevistas de iOS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

O que é o cancelamento de Task e como gerenciá-lo?

Resposta esperada: O cancelamento de Task permite cancelar operações assíncronas em curso. As tarefas devem verificar periodicamente Task.isCancelled ou usar Task.checkCancellation() que lança um erro.

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

Pontos-chave:

  • Task.isCancelled: verificação não bloqueante (retorna um bool)
  • Task.checkCancellation(): lança CancellationError se cancelada
  • Modificador SwiftUI .task { }: cancelamento automático ao desaparecer a view
Cancelamento cooperativo

O Swift usa um modelo de cancelamento cooperativo: as tarefas não são forçosamente encerradas. O código deve verificar ativamente Task.isCancelled ou checkCancellation() para reagir ao cancelamento. Sem essas verificações, a tarefa continua indefinidamente.

Como usar o MainActor corretamente em um app SwiftUI?

Resposta esperada: Anotar os ViewModels com @MainActor para garantir que todas as atualizações de estado UI ocorram na thread principal. Usar @MainActor em funções individuais se apenas certas operações tocam a 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()
    }
}

Erro comum: Marcar a classe inteira como @MainActor quando apenas certos métodos tocam a UI. Isso força todo o código na thread principal, incluindo operações pesadas que deveriam estar em background.

Como gerenciar data races com Sendable?

Resposta esperada: O protocolo Sendable garante que um tipo pode ser compartilhado entre tarefas sem risco de data race. Tipos por valor (struct, enum) são automaticamente Sendable, classes precisam ser final com propriedades imutáveis ou protegidas.

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

Regras Sendable:

  • Struct/Enum com propriedades Sendable: automaticamente Sendable
  • Classes: precisam ser final + imutáveis, ou usar @unchecked Sendable com proteção manual (locks, actors)
  • Closures: automaticamente Sendable se capturam apenas tipos Sendable
@unchecked Sendable

@unchecked Sendable desativa as verificações do compilador. Usar apenas se a thread-safety for garantida manualmente (locks, serial queues). É responsabilidade do desenvolvedor evitar data races.

Pronto para mandar bem nas entrevistas de iOS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Conclusão

Dominar Swift Structured Concurrency tornou-se essencial para entrevistas iOS em 2026. Os recrutadores avaliam três níveis: compreensão dos conceitos (async/await vs callbacks), domínio dos padrões (TaskGroup, isolamento de actor) e debugging (cancelamento, Sendable).

Checklist de preparação:

  • ✅ Explicar async/await vs DispatchQueue com exemplo concreto
  • ✅ Demonstrar o uso de TaskGroup para operações paralelas
  • ✅ Implementar um actor thread-safe para proteger estado mutável
  • ✅ Gerenciar erros em contexto concorrente (Result, throwing)
  • ✅ Diferenciar Task, Task.detached e async let com casos de uso
  • ✅ Implementar um timeout em uma operação assíncrona
  • ✅ Usar MainActor corretamente em uma arquitetura SwiftUI
  • ✅ Compreender Sendable e evitar data races

Os melhores candidatos combinam teoria e prática: explicam o "porquê" (evitar data races, melhorar legibilidade) e o "como" (código funcional com tratamento de erros). Praticar em projetos reais para consolidar esses padrões.

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

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

Compartilhar

Artigos relacionados