# Grand Central Dispatch (GCD) in Swift: Queues, Async, and Sync

Source: https://devdocs10.com/articles/swift/swift-gcd  
By Murat Bo · Updated 2026-10-17 · DevDocs10 (https://devdocs10.com)

Grand Central Dispatch, usually called GCD, is Swift’s low-level concurrency system for running work on queues. It helps you move expensive tasks off the main thread, keep apps responsive, and control when and where code executes.

> **Quick answer:** Use `DispatchQueue` to run work asynchronously on background or main queues. Use `async` for non-blocking work, `sync` only when you truly need to wait for the result, and avoid calling `sync` on the same queue because it can deadlock.

> **Difficulty:** Beginner to Intermediate
>
> **Helpful to know first:** You’ll understand this better if you know basic Swift syntax, how functions run in order, and the difference between the main thread and background work.

## 1. What Is Grand Central Dispatch (GCD)?

Grand Central Dispatch is a system for scheduling units of work on dispatch queues. A queue is a managed line of tasks, and GCD decides when to run those tasks on available threads.

- `DispatchQueue` is the main type you use in Swift.
- A `serial queue` runs one task at a time in order.
- A `concurrent queue` can run multiple tasks at the same time.
- The `main queue` is tied to the main thread and is used for UI work.

GCD is not a full architecture for your app. It does not replace good state management, and it does not magically make slow code fast. It gives you tools for scheduling and coordination.

## 2. Why GCD Matters

Without concurrency, a long-running task can block the main thread and freeze your interface. GCD lets you move work like file I/O, image decoding, or network result handling away from the UI thread.

That matters because users expect scrolling, taps, animations, and text input to stay smooth. If the main queue is busy, the app feels laggy even if it is still technically running.

GCD also helps coordinate multiple tasks. You can run independent work in parallel, then switch back to the main queue to update the UI safely.

## 3. Basic Syntax

The most common GCD pattern is to dispatch work asynchronously. You place the task inside a closure and send it to a queue.

### Minimal async example

```swift
let backgroundQueue = DispatchQueue(label: "com.example.background")

backgroundQueue.async {
    // Work runs later on this queue.
    print("Running in the background")
}

print("This line runs immediately")
```

Output (tested with Swift 6.4):

```text
This line runs immediately
```

The key idea is that `async` schedules work and returns immediately. The caller continues without waiting.

### Common queue types

```swift
let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global(qos: .userInitiated)
```

`DispatchQueue.main` is for UI-related work. A global queue is a shared system queue for background tasks with a quality-of-service level.

## 4. Step-by-Step Examples

### Example 1: Updating the UI after background work

Do the slow work off the main queue, then hop back to the main queue for UI updates. This is one of the most common GCD patterns in Swift.

```swift
func loadProfile() {
    DispatchQueue.global(qos: .userInitiated).async {
        let name = "Taylor"
        DispatchQueue.main.async {
            print("Show name on screen: " + name)
        }
    }
}
```

This pattern keeps the interface responsive while still letting you update visible state safely on the main queue.

### Example 2: Running tasks in order with a serial queue

Use a serial queue when tasks must not overlap, such as writing to a file or updating a shared resource in sequence.

```swift
let logQueue = DispatchQueue(label: "com.example.logQueue")

for message in ["Start", "Loading", "Done"] {
    logQueue.async {
        print(message)
    }
}
```

Because the queue is serial, the messages execute one by one in the order they were submitted.

### Example 3: Waiting for a result with sync

The `sync` method blocks the current thread until the queued task finishes. That can be useful in narrow cases, but you must be careful.

```swift
let queue = DispatchQueue(label: "com.example.syncExample")

let value = queue.sync {
    return 42
}

print(value)
```

Output (tested with Swift 6.4):

```text
42
```

This example returns a value immediately to the caller only after the closure completes. Use this sparingly, because blocking can hurt performance and can deadlock in the wrong context.

### Example 4: Delaying work with asyncAfter

GCD can schedule work to happen after a delay. This is useful for reminders, simple debouncing, or small timed effects.

```swift
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("Run after 2 seconds")
}
```

The code above waits two seconds before running on the main queue, which is useful when the delayed task affects the interface.

## 5. Practical Use Cases

- Loading and parsing JSON from a network response without blocking the UI.
- Processing images, thumbnails, or other CPU-heavy tasks in the background.
- Writing logs or files in a predictable order with a serial queue.
- Coordinating multiple independent jobs before continuing with a `DispatchGroup`.
- Protecting shared state with a private queue instead of letting multiple threads mutate it directly.

GCD is especially useful when you need direct control over scheduling and execution order, but not necessarily a higher-level abstraction.

## 6. Common Mistakes

### Mistake 1: Updating the UI from a background queue

UI work should happen on the main queue. Running interface code elsewhere can lead to glitches, warnings, or unpredictable behavior.

**Problem:** The background queue finishes the work and then tries to touch UI-related state from outside the main thread.

```swift
DispatchQueue.global(qos: .background).async {
    let title = "Loaded"
    print("Set label text to " + title)
}
```

**Fix:** Finish the background task first, then dispatch the UI update back to `DispatchQueue.main`.

```swift
DispatchQueue.global(qos: .background).async {
    let title = "Loaded"
    DispatchQueue.main.async {
        print("Set label text to " + title)
    }
}
```

The corrected version works because UI changes are made on the main queue, where user interface code belongs.

### Mistake 2: Calling sync on the same serial queue

If a serial queue tries to synchronously dispatch work onto itself, the queue waits for a task that can never start. This is one of the classic GCD deadlocks.

**Problem:** The outer task holds the serial queue, and the inner `sync` call waits forever for the same queue to become free.

```swift
let queue = DispatchQueue(label: "com.example.deadlock")

queue.async {
    queue.sync {
        print("This never runs")
    }
}
```

**Fix:** Use `async` instead, or redesign so the work does not require re-entering the same queue synchronously.

```swift
let queue = DispatchQueue(label: "com.example.safe")

queue.async {
    queue.async {
        print("This runs without deadlocking")
    }
}
```

The fix works because asynchronous dispatch does not block the current task while waiting.

### Mistake 3: Assuming concurrent queues make shared state safe

A concurrent queue can run several tasks at once, but that does not protect shared mutable data by itself. If multiple tasks write to the same variable, you can get race conditions.

**Problem:** Multiple tasks mutate `count` at the same time, so the final result is not reliable.

```swift
var count = 0
let queue = DispatchQueue(label: "com.example.concurrent", attributes: .concurrent)

for _ in 0..<1000 {
    queue.async {
        count += 1
    }
}
```

**Fix:** Serialize access to the shared value, or use a safer synchronization strategy.

```swift
var count = 0
let queue = DispatchQueue(label: "com.example.safeCounter")

for _ in 0..<1000 {
    queue.sync {
        count += 1
    }
}

print(count)
```

Output (tested with Swift 6.4):

```text
1000
```

The corrected version works because the serial queue ensures only one mutation happens at a time.

## 7. Best Practices

### Practice 1: Prefer async for most work

Asynchronous dispatch is usually the safest choice because it keeps the current thread free. Reserve `sync` for cases where blocking is intentional and safe.

```swift
DispatchQueue.global(qos: .utility).async {
    // Do background work here.
}
```

This keeps responsiveness high and reduces the chance of deadlocks.

### Practice 2: Use descriptive queue labels

A meaningful label helps you debug and inspect your app when you are tracking concurrency issues.

```swift
let imageProcessingQueue = DispatchQueue(label: "com.example.imageProcessing")
```

Clear labels make logs and debugging tools easier to understand.

### Practice 3: Keep shared mutable state behind one queue

If several tasks need to read and write the same data, funnel those accesses through a single serial queue instead of scattering mutations across threads.

```swift
final class SafeCounter {
    private var value = 0
    private let queue = DispatchQueue(label: "com.example.safeCounter")

    func increment() {
        queue.sync {
            value += 1
        }
    }

    func currentValue() -> Int {
        return queue.sync {
            value
        }
    }
}
```

This approach avoids data races because every access goes through one controlled path.

## 8. Limitations and Edge Cases

- GCD gives you queue-based scheduling, but it does not automatically make code thread-safe.
- Calling `sync` from the main queue to the main queue can freeze the app.
- Work submitted with `async` may not complete in the order you expect on a concurrent queue.
- Global queues are shared system resources, so you should not assume exclusive ownership or special ordering.
- GCD is low-level. For higher-level cancellation, structured task relationships, or modern async flows, Swift concurrency may be a better fit in new code.

One common “not working” scenario is when developers expect a concurrent queue to guarantee the order of completion. It only guarantees that tasks can overlap, not that they finish in submission order.

## 9. Practical Mini Project

Here is a small command-line style example that simulates loading multiple pieces of data in the background, then prints a final result once all jobs finish.

```swift
import Foundation

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.example.downloads", attributes: .concurrent)
var results: [String] = []
let resultsQueue = DispatchQueue(label: "com.example.results")

for item in ["profile", "posts", "messages"] {
    group.enter()
    queue.async {
        // Simulate background work.
        let result = "Loaded " + item
        resultsQueue.sync {
            results.append(result)
        }
        group.leave()
    }
}

group.notify(queue: .main) {
    print("All jobs finished:", results)
}

RunLoop.main.run()
```

This example shows how GCD can coordinate several background tasks and then deliver the final callback on the main queue. It also shows why shared mutable data still needs protection.

## 10. Key Points

- GCD schedules work on dispatch queues, not directly on threads.
- `async` is the usual choice for background work because it does not block the caller.
- The main queue is for UI updates and other main-thread-only tasks.
- Serial queues preserve order; concurrent queues allow overlap.
- Shared mutable state still needs synchronization even when GCD is involved.

## 11. Practice Exercise

Try this exercise to reinforce the core ideas.

- Create a serial queue labeled `com.example.exercise`.
- Dispatch three print tasks asynchronously to that queue.
- After the last task, dispatch a final message back to the main queue.
- Verify that the tasks run in order.

**Expected output:** The three task messages appear in order, followed by a final completion message.

**Hint:** Use a loop for the task submissions, and call `DispatchQueue.main.async` for the last message.

**Solution:**

```swift
import Foundation

let queue = DispatchQueue(label: "com.example.exercise")

for number in 1..<4 {
    queue.async {
        print("Task", number)
    }
}

DispatchQueue.main.async {
    print("All tasks submitted")
}

RunLoop.main.run()
```

This solution demonstrates ordered execution on a serial queue and a safe return to the main queue for the final message.

## 12. Final Summary

Grand Central Dispatch is Swift’s foundational queue-based concurrency tool. It helps you move work off the main thread, coordinate tasks, and control execution order with serial and concurrent queues.

The most important habits are simple: use `async` for most background work, switch back to `DispatchQueue.main` for UI updates, and avoid blocking a queue with the wrong kind of `sync` call. Those rules prevent the most common bugs and performance issues.

As you continue, practice combining queues with `DispatchGroup`, `DispatchWorkItem`, and safe state handling. Once those ideas feel natural, GCD becomes a powerful and predictable tool for real Swift apps.
