DispatchQueue is a fundamental queue of Grand Central Dispatch (GCD) for managing asynchronous tasks in iOS and macOS. According to Apple Developer Documentation, 2026, DispatchQueue abstracts thread management from the developer through serial and concurrent queues. GCD automatically distributes tasks across the system thread pool, eliminating the need for manual thread creation and destruction.
Key Takeaways
DispatchQueue is an object of the Grand Central Dispatch (GCD) framework that manages task execution in system or custom thread queues. Grand Central Dispatch is a low-level Apple library, available since iOS 4 and macOS 10.6, that completely abstracts thread management from the developer. GCD uses the operating system thread pool and automatically scales the number of threads according to device load.
The developer does not need to create and destroy threads manually — GCD handles this task, providing a simple API through DispatchQueue. A task in the form of a closure is submitted to a queue via sync or async methods. In the first case, the calling thread is blocked until the task completes; in the second, execution continues immediately.
According to Apple (2026), GCD uses a system thread pool that adapts to the number of cores and current CPU load. A concurrent queue does not create a new thread for each task — GCD reuses threads from the pool, minimizing the overhead of thread creation.
Grand Central Dispatch consists of three key components: queues (DispatchQueue), groups (DispatchGroup), and semaphores (DispatchSemaphore). The queue is the primary element that accepts tasks in the form of code blocks. DispatchGroup synchronizes the execution of multiple tasks, while DispatchSemaphore limits access to a shared resource to a specific number of threads.
Each GCD queue is associated with a specific QoS (Quality of Service) class, which informs the system about the importance of the task. The system uses QoS to distribute CPU time among queues, prioritizing more critical tasks — such as UI updates or user touch handling.
A serial queue executes tasks strictly sequentially, one after another. If three tasks are placed in a serial queue, the second one starts only after the first has fully completed. Serial queues are used for synchronizing access to shared resources — for example, an array modified from multiple parts of the code.
A concurrent queue runs multiple tasks simultaneously, distributing them across available threads from the system pool. Tasks on a concurrent queue start in FIFO order but complete in arbitrary order if their execution times differ. A concurrent queue does not guarantee completion order — only the start order.
| Parameter | Serial Queue | Concurrent Queue |
|---|---|---|
| Execution Order | Strictly sequential | Parallel |
| Number of Threads | One | Multiple from the GCD pool |
| Use Case | Protecting shared resources | Independent computations |
| Main Queue | Yes (main thread) | No |
| Deadlock Risk | High when syncing on the same queue | Low |
A serial queue is ideal for tasks that modify shared state — writing to a file, updating a data model, or working with Core Data. Using a serial queue guarantees that two pieces of code will not modify the same data simultaneously, eliminating race conditions without additional locks.
A concurrent queue is suitable for tasks that do not depend on each other: loading multiple images, parallel network requests, or batch data processing. GCD automatically decides how many tasks to run simultaneously based on the number of CPU cores and the current system load.
QoS (Quality of Service) is a GCD mechanism that informs the operating system about the importance and urgency of a task. The system uses QoS for thread scheduling: tasks with higher QoS get more CPU time and start earlier. The QoS value is passed when creating a queue or submitting a specific task.
GCD has five QoS classes available. .userInteractive — the highest priority for UI-related tasks. .userInitiated — for tasks initiated by the user. .utility — for background tasks that show progress. .background — for tasks invisible to the user. .default — an intermediate level between userInitiated and utility, used by default.
According to Apple (2026), incorrect QoS selection is one of the common causes of performance issues. Running a background download with QoS .userInteractive consumes resources needed by the UI, causing micro-lags in animations. It is recommended to choose the lowest QoS that still provides acceptable execution time.
When loading an image for immediate display, use .userInitiated — the user expects the result. For preloading the next screen, .utility is sufficient. Background server synchronization runs with .background, minimizing impact on active tasks.
DispatchGroup allows tracking the completion of a group of tasks. When all tasks in the group finish, GCD calls a notify handler on the specified queue. This is especially useful when loading multiple independent resources — profile data, friend list, and settings — where the interface should only update after all data is received.
DispatchGroup supports a synchronous wait() call, which blocks the current thread until all tasks complete. This is convenient when code cannot continue without the group's results. The asynchronous notify() variant calls a closure on the specified queue after all tasks finish, without blocking the calling thread.
DispatchSemaphore controls access to a resource by limiting the number of concurrent accesses. A semaphore with an initial value of 3 allows no more than three parallel tasks. Calling wait() decrements the counter, signal() increments it. If the counter reaches zero, the thread blocks until a resource becomes available.
Let's look at three practical examples of using DispatchQueue in Swift. The first demonstrates a basic async call with a return to the main thread, the second shows synchronization via a serial queue, and the third uses DispatchGroup for parallel requests.
DispatchQueue.main is the serial queue of the main thread, intended exclusively for UI operations. Always use it to update the interface after completing background work.
let queue = DispatchQueue.global(qos: .userInitiated)
queue.async {
let data = self.fetchData()
DispatchQueue.main.async {
self.updateUI(with: data)
}
}
Creating a custom serial queue with a unique identifier synchronizes access to a mutable array. All read and write operations go through a single queue, eliminating race conditions.
let serialQueue = DispatchQueue(label: "com.app.items")
var items: [Int] = []
serialQueue.async {
items.append(1)
}
serialQueue.async {
let last = items.last
DispatchQueue.main.async {
print("Last item: \(last)")
}
}
DispatchGroup allows launching multiple tasks on a concurrent queue and receiving a notification when all complete. This is useful when loading data for a profile screen.
let group = DispatchGroup()
let worker = DispatchQueue.global()
worker.async(group: group) { self.loadProfile() }
worker.async(group: group) { self.loadFriends() }
worker.async(group: group) { self.loadSettings() }
group.notify(queue: DispatchQueue.main) {
self.showCompleteUI()
}
Deadlock when calling sync on a serial queue is the most common mistake. If a task on a serial queue calls queue.sync on the same queue, the thread blocks forever. The queue waits for the current task to finish, and the task waits for the sync call — a classic mutual deadlock.
All operations with UIKit must be performed on the main thread. Xcode detects such errors in Debug mode via the Main Thread Checker. In Release builds, they lead to unpredictable behavior: animations don't start, the UI doesn't update, and crashes may occur.
Creating hundreds of custom queues instead of using global queues is an antipattern. Each queue consumes system resources. For most tasks, global concurrent queues with different QoS levels and one or two serial queues for synchronizing shared data are sufficient.
When performing resource-intensive loop tasks on a background queue without autoreleasepool, memory grows until the entire loop finishes. ARC only releases objects when leaving the autorelease pool. Wrap loop iterations in autoreleasepool { } for timely memory deallocation.
Frequently Asked Questions
OperationQueue is built on top of GCD but provides a higher-level API with operation dependencies, KVO, and cancellation support. DispatchQueue is a low-level queue for simple async tasks without dependency management.
GCD does not support stopping a running task. The suspend() method only pauses new tasks; the current one runs to completion. Cancellation requires manual flag checking inside the task code.
For a primary request with immediate result display — .userInitiated. For data preloading — .utility. For background synchronization — .background.
GCD does not fix the number of threads. The thread pool dynamically scales under load, considering CPU cores, current load, and the QoS of each task. The maximum number is limited by the system.
UIKit is not thread-safe — all its classes must only be called from the main thread. Violation leads to unpredictable behavior, missed updates, and crashes in Production.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also