DispatchQueue: What It Is, GCD Queue and Multithreading Basics

Author: IT Sectr Published: 2026-03-16 Reading time: 8 min

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 the main GCD abstraction for asynchronous code execution in iOS
  • Serial queue executes tasks strictly sequentially, eliminating race conditions
  • Concurrent queue runs multiple tasks in parallel via the system thread pool
  • QoS sets task priority — from userInteractive to background
  • DispatchQueue.main is the only queue for updating UIKit on the main thread

What Is DispatchQueue and Grand Central Dispatch

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.

GCD Architecture

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.

Serial vs Concurrent Queues: Comparison

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.

ParameterSerial QueueConcurrent Queue
Execution OrderStrictly sequentialParallel
Number of ThreadsOneMultiple from the GCD pool
Use CaseProtecting shared resourcesIndependent computations
Main QueueYes (main thread)No
Deadlock RiskHigh when syncing on the same queueLow

When to Choose a Serial Queue

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.

When to Choose a Concurrent Queue

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.

Quality of Service: Task Execution Priorities

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.

QoS Usage Example

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 and Semaphores: Task Synchronization

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 for Limiting Concurrency

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.

Code Examples with DispatchQueue in Swift

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.

Basic Async Call with Return to Main

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.

swift
let queue = DispatchQueue.global(qos: .userInitiated)
queue.async {
    let data = self.fetchData()
    DispatchQueue.main.async {
        self.updateUI(with: data)
    }
}

Serial Queue for Protecting a Shared Resource

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.

swift
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 for Parallel Requests

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.

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

Common Mistakes When Working with DispatchQueue

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.

Updating UI from a Background Thread

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.

Excessive Creation of Custom Queues

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.

Ignoring Autoreleasepool in Loops

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

What is the difference between DispatchQueue and OperationQueue?

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.

Can a task in DispatchQueue be forcibly stopped?

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.

Which QoS should I choose for a network request?

For a primary request with immediate result display — .userInitiated. For data preloading — .utility. For background synchronization — .background.

How many threads does a concurrent queue use?

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.

Why is DispatchQueue.main mandatory for UIKit?

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

  • DispatchQueue is the primary tool of Grand Central Dispatch for asynchronous tasks in iOS and macOS
  • Serial queue executes tasks sequentially, eliminating race conditions without locks
  • Concurrent queue runs tasks in parallel via the system thread pool
  • QoS determines task priority — from userInteractive to background
  • DispatchGroup synchronizes multiple parallel tasks with notify on the main thread
  • Deadlock when syncing on a busy serial queue is a critical error requiring attention
  • Main thread is mandatory for UIKit — update the interface only through DispatchQueue.main

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.

Discuss the project

Read also