Grand Central Dispatch (GCD), exposed through the Dispatch framework, executes work on queues managed by the system. Except for the main queue, a dispatch queue does not guarantee which thread runs a task.
GCD remains useful for queue-based synchronization, bridging callback APIs, integrating with older code, and handling low-level system events with dispatch sources. For new asynchronous workflows, prefer Swift concurrency with async/await, tasks, actors, and @MainActor.
Submit closures or DispatchWorkItem values to a DispatchQueue. Queues are thread-safe and can be serial or concurrent.
Serial queues guarantee that only one task runs at any given time. GCD controls the execution timing. You won’t know the amount of time between one task ending and the next one beginning.
Concurrent queues allow multiple tasks to run at the same time. Do not rely on completion order, thread identity, or the number of tasks running simultaneously.
Main queue runs on the main thread and is a serial queue.
Global queues are system-defined concurrent queues selected by quality-of-service (QoS) class.
Custom queues are queues you create. They are serial by default; pass .concurrent when you need a private concurrent queue.
Apple recommends avoiding excessive private concurrent queues. Blocking tasks can cause Dispatch to create additional threads, and too many blocked tasks can exhaust your app’s thread resources.
QoS communicates the importance of work to the system:
QoS affects scheduling priority. It does not select a specific CPU core or guarantee a thread.
async submits work and returns immediately:
DispatchQueue.global(qos: .userInitiated).async {
let image = renderPreview()
DispatchQueue.main.async {
imageView.image = image
}
}sync submits work and blocks the caller until the closure finishes:
let snapshot = stateQueue.sync {
cachedState
}Do not call sync on the main queue. Do not call sync on a serial queue from code already running on that queue. Both cases deadlock. Avoid blocking queue threads while waiting for asynchronous work.
You can submit a closure directly or wrap work in a DispatchWorkItem. A work item supports configuration such as QoS, barrier behavior, and cancellation state.
Use asyncAfter(deadline:) to schedule work after a delay. Do not use timing delays to hide race conditions.
Use a barrier as a synchronization point on a custom concurrent queue. Previously submitted tasks finish before the barrier starts. The barrier then runs alone, and later tasks resume afterward.
final class PhotoStore: @unchecked Sendable {
private let queue = DispatchQueue(
label: "com.example.photos",
attributes: .concurrent
)
private var photos: [Photo] = []
func snapshot() -> [Photo] {
queue.sync { photos }
}
func add(_ photo: Photo) {
queue.async(flags: .barrier) {
self.photos.append(photo)
}
}
}Use barriers with a private concurrent queue that your code controls. Do not use a global queue as a read/write lock.
@unchecked Sendable is appropriate here only because every access to photos goes through the queue. Treat this conformance as an audited promise that the synchronization remains correct.
Use DispatchGroup to monitor multiple tasks as one unit. Prefer notify(queue:) when possible because wait() blocks the current thread.
let group = DispatchGroup()
for url in urls {
group.enter()
download(url) { result in
defer { group.leave() }
handle(result)
}
}
group.notify(queue: .main) {
reloadUI()
}Completion handlers may arrive concurrently, so handle(_:) must also be safe to call concurrently.
Canceling a DispatchWorkItem prevents future execution if the item has not started. Cancellation does not stop a block that is already running.
let workItem = DispatchWorkItem {
performExpensiveWork()
}
workItem.cancel()
DispatchQueue.global(qos: .utility).async(execute: workItem)For cancellable asynchronous application logic, prefer Swift Task, whose operation can check Task.isCancelled or call Task.checkCancellation().
Dispatch sources process low-level system events asynchronously. A source submits its event handler to a queue when relevant events occur. Common uses include:
Use Swift concurrency for new asynchronous application logic when possible:
async/await for asynchronous operations.@MainActor for UI-facing state and updates.Use GCD when you need dispatch-specific behavior such as queue barriers, dispatch sources, semaphores, integration with existing queue-based code, or a synchronous critical section. Avoid mixing blocking waits with Swift concurrency.