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:
// 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:
// 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, whileserialQueue.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, butsync
on a concurrent queue still blocks the caller, though other tasks on the queue can proceed.
Example:
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:
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:
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()
andleave()
track each API call.notify
runs on the main queue after all calls complete.NSLock
ensures thread-safe array updates.
Example with OperationQueue:
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:
Aspect | GCD | OperationQueue |
---|---|---|
Abstraction Level | Low-level, queue-based. | High-level, operation-based. |
Task Representation | Closures dispatched to queues. | Operation objects encapsulating tasks. |
Dependency Management | Manual (e.g., using DispatchGroup ). | Built-in via addDependency(_:) . |
Cancellation | Manual (check flags or use DispatchWorkItem.cancel() ). | Built-in via cancel() and cancelAllOperations() . |
State Management | Stateless, developer-managed. | Tracks isExecuting , isFinished , isCancelled . |
Priority | QoS levels (e.g., .userInteractive ). | queuePriority and qualityOfService . |
Concurrency Control | Serial or concurrent queues. | maxConcurrentOperationCount . |
Suspension | Not directly supported. | isSuspended to pause/resume queue. |
Overhead | Lightweight, minimal overhead. | Higher overhead due to object management. |
Use Case | Simple, independent tasks (e.g., network calls). | Complex workflows with dependencies (e.g., download → process). |
Example:
// 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:
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:
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):
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
, oros_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:
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:
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:
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.