Pertanyaan Wawancara Swift Structured Concurrency: async/await, TaskGroup, Actors

Pertanyaan wawancara teknis Swift Structured Concurrency: async/await, TaskGroup, actors dan pola konkurensi untuk iOS 2026

Pertanyaan wawancara Swift Structured Concurrency dengan async/await, TaskGroup dan actors

Structured Concurrency yang diperkenalkan di Swift 5.5 telah merevolusi pemrograman asinkron di iOS. Para perekrut kini menguji penguasaan async/await, TaskGroup, dan actors dalam wawancara teknis. Berikut adalah pertanyaan-pertanyaan penting beserta jawaban yang diharapkan untuk menonjol dalam wawancara.

Kompetensi utama yang diuji dalam wawancara

Para perekrut menilai tiga kompetensi: pemahaman konsep dasar (async/await, Task), penguasaan pola konkurensi (TaskGroup, actor isolation), dan kemampuan mendiagnosis kesalahan umum (data races, deadlocks).

Apa perbedaan antara async/await dan DispatchQueue?

Jawaban yang diharapkan: async/await menyediakan structured concurrency dengan kode sekuensial yang mudah dibaca, sementara DispatchQueue menggunakan callback dan dapat menyebabkan apa yang disebut "callback hell". Swift mengelola thread secara otomatis dengan 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)
}

Poin-poin utama: async/await menghilangkan piramida callback, mengurangi kesalahan thread (tidak perlu DispatchQueue.main.async), dan memungkinkan runtime Swift mengoptimalkan eksekusi di seluruh inti CPU yang tersedia.

Keunggulan kinerja

Runtime Swift menggunakan thread pool yang dioptimalkan yang menghindari pembuatan thread berlebihan. Berbeda dengan DispatchQueue di mana setiap .async dapat membuat thread baru, async/await menggunakan kembali thread yang ada secara cerdas.

Bagaimana mengelola beberapa operasi asinkron secara paralel?

Jawaban yang diharapkan: Gunakan async let untuk 2-3 task sederhana, atau TaskGroup untuk jumlah task paralel yang dinamis dengan pengumpulan hasil.

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

Kesalahan umum: Menggunakan await secara sekuensial alih-alih async let untuk memparalelkan panggilan. let user = await fetchUser(); let posts = await fetchPosts() berjalan sekuensial (lambat), sedangkan async let meluncurkan keduanya secara bersamaan.

Apa itu actor dan mengapa menggunakannya?

Jawaban yang diharapkan: Actor adalah tipe yang melindungi state mutable-nya dari data race dengan menjamin akses sekuensial. Ia menggantikan lock manual (NSLock, DispatchQueue) untuk mengamankan akses bersamaan.

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

Poin-poin utama: Actor menjamin hanya satu thread yang mengakses state-nya pada satu waktu. Compiler memaksa penggunaan await untuk panggilan eksternal, sehingga titik suspensi potensial menjadi eksplisit.

Jebakan MainActor

@MainActor adalah actor global untuk operasi UI. Menandai sebuah class sebagai @MainActor memaksa semua metodenya berjalan di main thread. Hati-hati dengan panggilan blocking yang dapat membekukan antarmuka.

Siap menguasai wawancara iOS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Bagaimana menangani kesalahan dalam TaskGroup?

Jawaban yang diharapkan: withThrowingTaskGroup menyebarkan kesalahan pertama yang ditemui dan secara otomatis membatalkan task yang tersisa. Untuk mengumpulkan semua kesalahan, gunakan Result dalam 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)
    }
}

Poin utama: withThrowingTaskGroup berhenti pada kesalahan pertama (berguna untuk operasi atomik), sedangkan withTaskGroup + Result memungkinkan untuk melanjutkan meskipun ada kesalahan (berguna untuk pemrosesan batch).

Apa perbedaan antara Task, Task.detached, dan async let?

Jawaban yang diharapkan: Task mewarisi konteks induk (prioritas, actor isolation), Task.detached membuat task independen tanpa pewarisan, dan async let membuat child task yang secara otomatis ditunggu di akhir scope.

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

Kasus penggunaan:

  • Task: Operasi yang terikat pada konteks saat ini (misalnya, pembaruan UI dari ViewModel)
  • Task.detached: Task background independen (misalnya, log, analytics)
  • async let: Operasi paralel dengan hasil yang dibutuhkan dalam scope saat ini

Bagaimana mengimplementasikan timeout pada operasi asinkron?

Jawaban yang diharapkan: Gunakan Task.sleep dalam balapan antara task utama dan task timeout dengan withThrowingTaskGroup, atau buat utilitas 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)
    }
}

Alternatif modern: Mulai iOS 16, gunakan URLSession dengan timeoutInterval yang dikonfigurasi melalui URLSessionConfiguration khusus untuk panggilan HTTP.

Pembatalan eksplisit

group.cancelAll() sangat penting untuk membebaskan sumber daya. Tanpa itu, task yang kalah akan terus berjalan di background hingga selesai secara alami, membuang CPU dan memori.

Bagaimana berbagi state mutable dengan aman antara beberapa task?

Jawaban yang diharapkan: Gunakan actor untuk state bersama, atau AsyncStream untuk berkomunikasi antar task melalui aliran nilai.

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

Pilihan arsitektur: Actor untuk state terpusat dengan logika bisnis, AsyncStream untuk komunikasi berbasis event antara komponen yang terdekoupling.

Siap menguasai wawancara iOS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Apa itu pembatalan Task dan bagaimana menanganinya?

Jawaban yang diharapkan: Pembatalan Task memungkinkan untuk membatalkan operasi asinkron yang sedang berjalan. Task harus secara berkala memeriksa Task.isCancelled atau menggunakan Task.checkCancellation() yang melempar kesalahan.

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

Poin-poin utama:

  • Task.isCancelled: pemeriksaan non-blocking (mengembalikan bool)
  • Task.checkCancellation(): melempar CancellationError jika dibatalkan
  • Modifier SwiftUI .task { }: pembatalan otomatis saat view menghilang
Pembatalan kooperatif

Swift menggunakan model pembatalan kooperatif: task tidak dipaksa untuk dihentikan. Kode harus secara aktif memeriksa Task.isCancelled atau checkCancellation() untuk merespons pembatalan. Tanpa pemeriksaan ini, task terus berjalan tanpa batas.

Bagaimana menggunakan MainActor dengan benar dalam aplikasi SwiftUI?

Jawaban yang diharapkan: Anotasi ViewModel dengan @MainActor untuk menjamin semua pembaruan state UI terjadi di main thread. Gunakan @MainActor pada fungsi individual jika hanya operasi tertentu yang menyentuh 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()
    }
}

Kesalahan umum: Menandai seluruh class sebagai @MainActor ketika hanya metode tertentu yang menyentuh UI. Ini memaksa semua kode di main thread, termasuk operasi berat yang seharusnya di background.

Bagaimana menangani data race dengan Sendable?

Jawaban yang diharapkan: Protokol Sendable menjamin bahwa sebuah tipe dapat dibagikan antar task tanpa risiko data race. Tipe nilai (struct, enum) secara otomatis Sendable, class harus final dengan properti immutable atau dilindungi.

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

Aturan Sendable:

  • Struct/Enum dengan properti Sendable: otomatis Sendable
  • Class: harus final + immutable, atau gunakan @unchecked Sendable dengan perlindungan manual (lock, actor)
  • Closure: otomatis Sendable jika hanya menangkap tipe Sendable
@unchecked Sendable

@unchecked Sendable menonaktifkan pemeriksaan compiler. Gunakan hanya jika thread-safety dijamin secara manual (lock, serial queue). Tanggung jawab developer untuk menghindari data race.

Siap menguasai wawancara iOS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Kesimpulan

Menguasai Swift Structured Concurrency telah menjadi penting untuk wawancara iOS pada 2026. Para perekrut menguji tiga tingkat: pemahaman konsep (async/await vs callback), penguasaan pola (TaskGroup, actor isolation), dan debugging (pembatalan, Sendable).

Daftar persiapan:

  • ✅ Jelaskan async/await vs DispatchQueue dengan contoh konkret
  • ✅ Tunjukkan penggunaan TaskGroup untuk operasi paralel
  • ✅ Implementasikan actor thread-safe untuk melindungi state mutable
  • ✅ Tangani kesalahan dalam konteks bersamaan (Result, throwing)
  • ✅ Bedakan Task, Task.detached, dan async let dengan kasus penggunaan
  • ✅ Implementasikan timeout pada operasi asinkron
  • ✅ Gunakan MainActor dengan benar dalam arsitektur SwiftUI
  • ✅ Pahami Sendable dan hindari data race

Kandidat terbaik menggabungkan teori dan praktik: menjelaskan "mengapa" (menghindari data race, meningkatkan keterbacaan) dan "bagaimana" (kode fungsional dengan penanganan kesalahan). Berlatih pada proyek nyata untuk memperkuat pola-pola ini.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

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

Bagikan

Artikel terkait