Skip to content

iOS Concurrency Interview Questions

Synchronous vs Asynchronous

Synchronous execution means tasks run sequentially, blocking the calling thread until the task completes. It’s suitable for quick operations but can freeze the UI if used on the main thread for long tasks.

Asynchronous execution allows tasks to run independently, freeing the calling thread to continue. It’s ideal for time-consuming tasks like network requests or file I/O to maintain app responsiveness.

Example:

swift
// Synchronous
DispatchQueue.global().sync {
    print("Sync task on \(Thread.current)") // Blocks calling thread
}

// Asynchronous
DispatchQueue.global().async {
    print("Async task on \(Thread.current)") // Non-blocking
}

Key Difference:

  • Synchronous: Blocks the thread, waits for completion.
  • Asynchronous: Non-blocking, continues immediately.

Use Case:

  • Synchronous: Quick, critical tasks like reading small in-memory data.
  • Asynchronous: Network calls, UI updates, or heavy computations.

Serial Queue vs Concurrent Queue

A Serial Queue executes tasks one at a time in the order they are added (FIFO). It’s thread-safe for shared resources but slower for independent tasks.

A Concurrent Queue executes tasks simultaneously, limited by system resources. Tasks may complete out of order, suitable for independent operations but requires synchronization for shared resources.

Example:

swift
// Serial Queue
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.async { print("Task 1") }
serialQueue.async { print("Task 2") } // Task 2 waits for Task 1

// Concurrent Queue
let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
concurrentQueue.async { print("Task 1") }
concurrentQueue.async { print("Task 2") } // Tasks may run simultaneously

Key Difference:

  • Serial: One task at a time, ordered execution.
  • Concurrent: Multiple tasks at once, unordered completion.

Use Case:

  • Serial: Updating a shared database or counter.
  • Concurrent: Processing multiple images or API calls in parallel.

Serial vs Synchronous and Concurrent vs Asynchronous

  • Serial vs Synchronous:

    • Serial refers to queue type: tasks execute one at a time in order.
    • Synchronous refers to execution style: blocks the calling thread.
    • A serial queue can run tasks synchronously or asynchronously. For example, serialQueue.sync blocks the caller, while serialQueue.async doesn’t.
  • Concurrent vs Asynchronous:

    • Concurrent refers to queue type: tasks can run simultaneously.
    • Asynchronous refers to execution style: non-blocking.
    • A concurrent queue typically uses async to leverage parallelism, but sync on a concurrent queue still blocks the caller, though other tasks on the queue can proceed.

Example:

swift
let serialQueue = DispatchQueue(label: "com.example.serial")
serialQueue.sync { print("Serial + Sync: Blocks") } // Blocks caller
serialQueue.async { print("Serial + Async: Non-blocking") } // Non-blocking

let concurrentQueue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)
concurrentQueue.async { print("Concurrent + Async: Parallel") } // Non-blocking, parallel
concurrentQueue.sync { print("Concurrent + Sync: Blocks but allows others") } // Blocks caller

Key Insight:

  • Serial/Concurrent describes queue behavior (order and concurrency).
  • Synchronous/Asynchronous describes thread behavior (blocking or non-blocking).

What is QoS, and Where Should You Use Which One?

Quality of Service (QoS) in GCD and Operation Queues specifies the priority and resource allocation for tasks, influencing how the system schedules them. QoS balances performance, battery life, and responsiveness by assigning tasks to different priority levels.

QoS Levels:

  • .userInteractive: Highest priority, for UI-related tasks requiring immediate responsiveness (e.g., animations, touch handling). Use sparingly to avoid resource contention.
  • .userInitiated: High priority, for user-triggered tasks needing quick results (e.g., loading data after a button tap).
  • .default: Medium priority, for general tasks with no specific urgency (e.g., background data syncing). Automatically assigned if no QoS is specified.
  • .utility: Lower priority, for tasks that can take longer but provide user feedback (e.g., downloading large files, progress bars).
  • .background: Lowest priority, for non-urgent tasks invisible to the user (e.g., cleanup, analytics reporting).

Example:

swift
DispatchQueue.global(qos: .userInteractive).async {
    print("Animating UI on \(Thread.current)")
}
DispatchQueue.global(qos: .background).async {
    print("Cleaning cache on \(Thread.current)")
}

Where to Use:

  • .userInteractive: Main thread UI updates, smooth scrolling, animations.
  • .userInitiated: Fetching data for a screen, search queries.
  • .default: General app logic, unspecified tasks.
  • .utility: File downloads, image processing with progress.
  • .background: Database maintenance, log uploads.

Best Practice: Match QoS to task urgency to optimize performance and battery life. Avoid overusing .userInteractive.

How Can You Make Multiple API Calls Together and Proceed Only on Completion of All?

To execute multiple API calls concurrently and proceed only after all complete, use a DispatchGroup or OperationQueue with dependencies. A DispatchGroup is simpler for GCD-based tasks, while OperationQueue is better for complex workflows with dependencies.

Example with DispatchGroup:

swift
let group = DispatchGroup()
var results: [String] = []
let lock = NSLock() // Thread-safe results

let urls = ["url1", "url2", "url3"]
for url in urls {
    group.enter()
    DispatchQueue.global(qos: .userInitiated).async {
        print("Fetching \(url) on \(Thread.current)")
        Thread.sleep(forTimeInterval: 1) // Simulate API call
        lock.withLock {
            results.append("Data from \(url)")
        }
        group.leave()
    }
}

group.notify(queue: .main) {
    print("All API calls complete: \(results)")
    // Update UI
}

Explanation:

  • enter() and leave() track each API call.
  • notify runs on the main queue after all calls complete.
  • NSLock ensures thread-safe array updates.

Example with OperationQueue:

swift
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
var results: [String] = []
let lock = NSLock()

var operations: [Operation] = []
for url in ["url1", "url2", "url3"] {
    let operation = BlockOperation {
        print("Fetching \(url) on \(Thread.current)")
        Thread.sleep(forTimeInterval: 1) // Simulate API
        lock.withLock {
            results.append("Data from \(url)")
        }
    }
    operations.append(operation)
}

let completionOp = BlockOperation {
    DispatchQueue.main.async {
        print("All API calls complete: \(results)")
        // Update UI
    }
}
operations.forEach { completionOp.addDependency($0) }

queue.addOperations(operations + [completionOp], waitUntilFinished: false)

Explanation:

  • Each API call is a BlockOperation.
  • A completionOp depends on all API operations, ensuring it runs last.
  • Results are aggregated thread-safely using NSLock.

Use Case: Fetching user profile, settings, and photos before displaying a dashboard.

Difference Between GCD and Operation Queue

Grand Central Dispatch (GCD) and OperationQueue are concurrency frameworks in iOS, but they serve different purposes:

AspectGCDOperationQueue
Abstraction LevelLow-level, queue-based.High-level, operation-based.
Task RepresentationClosures dispatched to queues.Operation objects encapsulating tasks.
Dependency ManagementManual (e.g., using DispatchGroup).Built-in via addDependency(_:).
CancellationManual (check flags or use DispatchWorkItem.cancel()).Built-in via cancel() and cancelAllOperations().
State ManagementStateless, developer-managed.Tracks isExecuting, isFinished, isCancelled.
PriorityQoS levels (e.g., .userInteractive).queuePriority and qualityOfService.
Concurrency ControlSerial or concurrent queues.maxConcurrentOperationCount.
SuspensionNot directly supported.isSuspended to pause/resume queue.
OverheadLightweight, minimal overhead.Higher overhead due to object management.
Use CaseSimple, independent tasks (e.g., network calls).Complex workflows with dependencies (e.g., download → process).

Example:

swift
// GCD
DispatchQueue.global().async {
    print("GCD task")
}

// OperationQueue
let queue = OperationQueue()
queue.addOperation {
    print("Operation task")
}

When to Use:

  • GCD: Quick, lightweight tasks or fine-grained control (e.g., background data fetch).
  • OperationQueue: Structured workflows with dependencies, cancellation, or progress tracking (e.g., multi-step data processing).

How Can You Make an Operation Asynchronous?

To make an Operation asynchronous, subclass Operation, override start() (instead of main()), set isAsynchronous = true, and manually manage KVO-compliant properties (isExecuting, isFinished). Use asynchronous APIs (e.g., URLSession) and ensure state updates are thread-safe.

Example:

swift
class AsyncDownloadOperation: Operation {
    private let url: URL
    private var imageData: Data?
    private var _executing = false
    private var _finished = false
    private var task: URLSessionDataTask?

    init(url: URL) {
        self.url = url
        super.init()
    }

    override var isAsynchronous: Bool { true }

    override var isExecuting: Bool {
        get { _executing }
        set {
            willChangeValue(forKey: "isExecuting")
            _executing = newValue
            didChangeValue(forKey: "isExecuting")
        }
    }

    override var isFinished: Bool {
        get { _finished }
        set {
            willChangeValue(forKey: "isFinished")
            _finished = newValue
            didChangeValue(forKey: "isFinished")
        }
    }

    override func start() {
        guard !isCancelled else {
            isFinished = true
            return
        }
        isExecuting = true
        task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
            defer {
                self?.isExecuting = false
                self?.isFinished = true
            }
            guard !self?.isCancelled ?? false else { return }
            if let error = error {
                print("Error: \(error)")
                return
            }
            self?.imageData = data
            print("Downloaded data from \(self?.url.absoluteString ?? "")")
        }
        task?.resume()
    }

    override func cancel() {
        super.cancel()
        task?.cancel()
        isExecuting = false
        isFinished = true
    }

    func getImageData() -> Data? { imageData }
}

let queue = OperationQueue()
let op = AsyncDownloadOperation(url: URL(string: "https://example.com/image.jpg")!)
queue.addOperation(op)

Explanation:

  • isAsynchronous = true enables async behavior.
  • start() initiates the network task and manages state.
  • cancel() stops the task and updates state.
  • KVO compliance ensures OperationQueue compatibility.

Use Case: Network requests, file I/O, or tasks involving external resources.

How Can You Add Dependencies Between Tasks?

Dependencies ensure tasks execute in a specific order. In OperationQueue, use addDependency(_:) to make one operation wait for another. In GCD, use DispatchGroup or manual synchronization for similar effects, though it’s less structured.

Example with OperationQueue:

swift
let queue = OperationQueue()
let downloadOp = BlockOperation { print("Downloading") }
let processOp = BlockOperation { print("Processing") }
processOp.addDependency(downloadOp)
queue.addOperations([downloadOp, processOp], waitUntilFinished: false)

Output:

Downloading
Processing

Example with GCD (DispatchGroup):

swift
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
    print("Task 1")
    group.leave()
}
group.notify(queue: .global()) {
    print("Task 2")
}

Explanation:

  • OperationQueue: Dependencies are explicit and managed by the queue.
  • GCD: DispatchGroup simulates dependencies by waiting for tasks to complete.

Best Practice: Use OperationQueue for complex dependencies; GCD for simpler synchronization.

How Can You Make a Class Thread Safe?

A class is thread-safe if it can be accessed concurrently by multiple threads without causing race conditions, data corruption, or crashes. Common techniques include:

  • Serial Queue: Use a serial dispatch queue to synchronize access.
  • Dispatch Barrier: Use barriers on concurrent queues for exclusive writes.
  • Semaphore/Lock: Use DispatchSemaphore, NSLock, or os_unfair_lock for critical sections.
  • Actors (Swift): Use Swift actors for automatic thread safety (Swift 5.5+).
  • Atomic Operations: Use atomic properties or libraries like libkern/OSAtomic (limited use).

Example with Serial Queue:

swift
class ThreadSafeCounter {
    private var count = 0
    private let queue = DispatchQueue(label: "com.example.counter")

    func increment() {
        queue.sync {
            count += 1
            print("Count: \(count)")
        }
    }

    func getCount() -> Int {
        queue.sync { count }
    }
}

Example with NSLock:

swift
class ThreadSafeArray {
    private var array: [Int] = []
    private let lock = NSLock()

    func append(_ value: Int) {
        lock.withLock {
            array.append(value)
            print("Appended \(value)")
        }
    }

    func getArray() -> [Int] {
        lock.withLock { array }
    }
}

Use Case: Managing shared resources like caches, counters, or data stores in multi-threaded apps.

Can You Update the UI on a Background Thread?

No, you cannot update the UI on a background thread in iOS. UIKit and AppKit are not thread-safe, and UI updates must occur on the main thread to avoid crashes, undefined behavior, or visual glitches.

Correct Approach: Dispatch UI updates to the main queue using DispatchQueue.main.async or DispatchQueue.main.sync (rarely).

Example:

swift
DispatchQueue.global().async {
    // Background work
    let data = "Fetched data"
    DispatchQueue.main.async {
        // UI update
        print("Updating UI with \(data) on \(Thread.current)")
    }
}

Why Main Thread?

  • UIKit components (e.g., UILabel, UIView) rely on the main run loop.
  • Background updates can cause race conditions or rendering issues.

Exception: Some Core Animation or Metal tasks can be prepared on background threads, but final presentation must be on the main thread.

Best Practice: Always verify Thread.isMainThread or use DispatchQueue.main for UI-related code.

Released under the MIT License.