02 · What does this print? — queues
Four short GCD puzzles on serial vs concurrent and sync vs async, including the two that crash.
Shape: what does this print · Reported: GCD questions at Revolut; this exact ordering puzzle is the standard warm-up form · Verified: run with Swift 6.4
"No running it. Tell me what prints, in what order, and which parts of the order are guaranteed."
Each puzzle starts on the main thread.
Puzzle A
let serial = DispatchQueue(label: "serial")
print("1")
serial.async { print("2") }
serial.sync { print("3") }
print("4")
DispatchQueue.global().async { print("5") }
print("6")Puzzle B
let serial = DispatchQueue(label: "serial")
serial.sync {
serial.sync { print("inner") }
}
print("after")Puzzle C
let concurrent = DispatchQueue(label: "c", attributes: .concurrent)
concurrent.sync {
concurrent.sync { print("inner") }
}
print("after")Puzzle D
// Inside viewDidLoad
DispatchQueue.main.sync { print("inside") }
print("after")A hint, if you're stuck
- There are two separate questions. Does the queue run one block at a time (serial) or several (concurrent)? Does the caller wait (sync) or not (async)?
- For each line, ask: who is waiting for whom?
- "It usually prints first" and "it's guaranteed to print first" are different answers.
Answers (verified output)
A — 1 2 3 4, then 5 and 6 in either order. 2 is queued before 3 on the same serial queue, and a serial queue runs one block at a time in order, so 3 waits for 2. sync blocks the caller until 3 finishes, so 4 comes after it. 5 goes to another thread and 6 doesn't wait for it: usually 6 prints first, but nothing guarantees it. In the verified run it printed 1 2 3 4 6 5 three times out of three — which is exactly the trap: "it always prints 6 first" is an observation, not a guarantee.
B — crash. The outer block occupies the serial queue; the inner sync waits for the queue to be free; the queue is waiting for the outer block to finish. That's a deadlock, and libdispatch detects this one and traps rather than hanging — verified: the process was killed by a trap (exit code 133). after never prints.
C — inner, then after. A concurrent queue can run more than one block at a time, so the inner block doesn't need the outer one to finish. No deadlock.
D — crash. Same as B: the main queue is serial and you are already on it.
The idea behind it
A queue is a line of work waiting to run. A serial queue runs one block at a time, in order — one cashier. A concurrent queue may run several at once — several cashiers. That's one question, and it's about the queue.
The other question is about you, the caller. async means "put my work in the line and walk away". sync means "stand here until my work is done".
Mix the two up and you get the classic deadlock. On a serial queue, a block calls sync onto the same queue. It waits for the queue to be free — but the queue is busy running that very block. Each waits for the other, forever. On the main queue that would freeze the app, so Apple's dispatch library spots it and crashes instead, which is easier to debug.
Ordering follows from the same picture. Inside one serial queue, order is guaranteed: one cashier, one line. Across different queues nothing is guaranteed unless something waits. A result that "always" comes out in one order on your machine is a race that hasn't lost yet.
How to make it unsurprising
let group = DispatchGroup()
DispatchQueue.global().async(group: group) { print("5") }
group.wait()
print("6") // always 5, then 6- If order matters, make something wait for it. A group, a serial queue or a completion — never "it usually comes first".
- Never
synconto a queue you might already be on. PutdispatchPrecondition(condition: .notOnQueue(queue))before async, and the mistake stops at that line with a clear message. - Update UI with
DispatchQueue.main.async, nevermain.sync.
What I'm really scoring
That you separate the two axes. Serial vs concurrent is about the queue: how many blocks it runs at once. Sync vs async is about the caller: whether it waits. Candidates who mix them up say things like "async means background" — DispatchQueue.main.async is async and runs on main.
The strong answer to A also says which orderings are guaranteed. That habit is what stops someone shipping code that only works because a race usually goes one way.
What I'd ask next
- "Why do we update UI with
main.asyncand nevermain.sync?" —syncfrom main deadlocks; from a background thread it blocks that thread while main is busy, and can deadlock if main is waiting on it. - "Make a thread-safe counter with GCD." — A private serial queue: reads with
sync, writes withasync; or a concurrent queue withasync(flags: .barrier)for writes. The barrier only works on a concurrent queue you created; on a global queue it's ignored and the writes race. - "What's the modern equivalent?" — An actor. Same mutual exclusion, checked by the compiler, and no possibility of puzzle B — but beware reentrancy at every
await. - "Is the main queue the same as the main thread?" — The main queue always runs on the main thread; other queues can run on any thread, including main when called with
sync.