iosNudge

What is the difference between serial and concurrent queues, and sync vs async?

EasyAsked oftenConcurrency 2 min read

TL;DR

Serial vs concurrent is about the queue: how many tasks run at the same time.

  • Serial: one at a time, in order.
  • Concurrent: many at once. They start in order but can finish in any order.

Sync vs async is about the caller: does it wait?

  • sync: the caller waits until the work is done.
  • async: the caller continues right away.

These are two separate ideas. Interviewers often check that you do not mix them up.

Serial queue

let serial = DispatchQueue(label: "com.app.serial")   // serial by default

serial.async { print("1") }
serial.async { print("2") }
serial.async { print("3") }
// Always: 1, 2, 3

Uses:

  • Protecting shared data. Only one task touches it at a time.
  • Work that must happen in order, like writing to a log file.

DispatchQueue.main is a serial queue that runs on the main thread.

Concurrent queue

let concurrent = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)

concurrent.async { print("A") }
concurrent.async { print("B") }
concurrent.async { print("C") }
// Any order, e.g. B, A, C

DispatchQueue.global(qos:) gives you system concurrent queues.

Uses: independent work that can run in parallel, like resizing many images.

sync vs async

let queue = DispatchQueue(label: "com.app.work")

print("before")
queue.async { print("inside async") }
print("after")
// before, after, inside async   (most likely)

print("before")
queue.sync { print("inside sync") }
print("after")
// before, inside sync, after    (always)

sync blocks the current thread until the closure finishes.

All four combinations

syncasync
SerialCaller waits; tasks run one by oneCaller continues; tasks run one by one
ConcurrentCaller waits; other tasks may run in parallelCaller continues; tasks run in parallel

Thread-safe property with a serial queue

final class Counter: @unchecked Sendable {
    private var value = 0
    private let queue = DispatchQueue(label: "com.app.counter")

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

    var current: Int {
        queue.sync { value }
    }
}

Writes are async (caller does not wait). Reads are sync (caller needs the value now).

Readers-writer with a concurrent queue and barrier

Many reads can run at once. A write must run alone.

final class Settings: @unchecked Sendable {
    private var store: [String: String] = [:]
    private let queue = DispatchQueue(label: "com.app.settings", attributes: .concurrent)

    func value(for key: String) -> String? {
        queue.sync { store[key] }                          // parallel reads
    }

    func set(_ value: String, for key: String) {
        queue.async(flags: .barrier) { self.store[key] = value }   // exclusive write
    }
}

A barrier waits for running tasks to finish, runs alone, then lets others continue. Barriers only work on custom concurrent queues, not global ones.

The classic deadlock

// Called on the main thread:
DispatchQueue.main.sync {
    print("never runs")
}

The main thread waits for the block. The block waits for the main thread to be free. Nobody moves. The same happens with sync onto any serial queue you are already on.

Modern Swift

  • Serial queue protecting state → an actor.
  • DispatchQueue.main.async → @MainActor / await MainActor.run.
  • Concurrent work → async let or a task group.

GCD is still common in older code and some APIs, so you need both.

Common follow-up questions

  • Is a serial queue tied to one thread? No (except main). GCD may run its tasks on different threads, but never two at once.
  • Does async on a concurrent queue create a new thread each time? No. GCD uses a pool of threads. But too many blocking tasks can cause thread explosion.
  • What is thread explosion? Many blocked tasks make GCD create many threads. This wastes memory and CPU. Swift concurrency avoids it with a fixed-size pool.
  • What is a target queue? A queue can forward its work to another queue. It is used to build queue hierarchies and limit parallelism.
  • Is sync from a background thread to main OK? It works, but it blocks the background thread and can deadlock if main is waiting on that thread. Prefer async.