On a serial queue, which prints are guaranteed to be ordered, what does a barrier guarantee, and what happens if a block calls sync on its own queue?
Predict first
What is the order of these prints?
import Foundation
let q = DispatchQueue(label: "serial")
q.async { print("A") }
q.sync { print("B") }
q.async { print("C") }
q.sync { print("D") }
print("E")
- A, B, C, D, E
- E, A, B, C, D
- A, B, E, C, D
Predict first
A barrier is submitted between two readers on a concurrent queue. What do they see?
import Foundation
let c = DispatchQueue(label: "concurrent", attributes: .concurrent)
nonisolated(unsafe) var value = 0
c.async { Thread.sleep(forTimeInterval: 0.1); print("reader before sees", value) }
c.async(flags: .barrier) { value = 42; print("barrier wrote") }
c.async { print("reader after sees", value) }
Thread.sleep(forTimeInterval: 0.5)
- reader before sees 0 / barrier wrote / reader after sees 42
- barrier wrote / reader before sees 42 / reader after sees 42
- reader after sees 0 / barrier wrote / reader before sees 42
Predict first
A block on a serial queue calls sync on that same queue. What happens?
import Foundation
let q = DispatchQueue(label: "serial")
q.async {
print("outer start")
q.sync { print("inner") }
print("outer end")
}
Thread.sleep(forTimeInterval: 0.5)
print("finished")
- It traps
- It prints outer start, inner, outer end, finished
- It hangs forever without a message
Mental model. A serial queue is a single-lane road: cars never overtake. A barrier is a level crossing that waits for the cars already on the road and stops the next ones. A self-sync is a car waiting for itself to move out of its own way.
A serial queue runs its blocks one at a time in the order they were submitted, so every print inside a block on that queue is ordered relative to the others: A, B, C, D come out in that order. sync adds one more guarantee: the caller waits until its block has finished, so “E” on the calling thread appears after D. What is not ordered is the calling thread against an async block that is still waiting: if the example printed “E” between the two sync calls it could land before or after “C”, which is why this version leaves that out.
On a concurrent queue, a .barrier block waits for everything submitted before it to finish and runs alone; blocks submitted after it start only once it has finished. So the slow reader (submitted first) prints 0, then the barrier writes, then the later reader sees 42. That is the reader-writer pattern, but only for queues you create (a barrier on a global queue behaves like a plain async). Finally, sync onto the queue you are already running on can never finish, because the outer block is waiting for the inner one and the inner one is waiting for the outer one. libdispatch detects this case and traps (the process dies with a signal) rather than hanging. Do not rely on that catching every deadlock: two different queues syncing onto each other, or a lock cycle, simply hang.
Serial queue order plus sync waits give A to D in order; E runs after the last sync returns.
The barrier waits for the earlier reader and holds back the later one.
libdispatch notices the queue is already owned by the current thread and traps instead of deadlocking silently.
Common traps
- Assuming the calling thread’s next line is ordered against pending async blocks. Only
sync, a group or a semaphore orders the caller. - Using a barrier on a global concurrent queue. It is not honoured there; use your own concurrent queue.
They might follow up with
- How do you ask “am I on this queue?” Prefer designing so you never need to;
dispatchPrecondition(condition: .onQueue(q))asserts it in debug builds. - Would an actor avoid this? An actor cannot be called synchronously from outside, and its reentrancy has different traps (see the actor puzzle).
Related questions
- 27.3.1 Blocks are scheduled on the main queue from the main thread, and one of them schedules another. In what order…
- 27.3.3 A screen stores a DispatchWorkItem whose closure uses self. Does cancelling the item fix the leak, and what…
- 27.4.1 Two tasks each withdraw 80 from an actor account holding 100, with an await between the balance check and the…
- 6.3.3 What is the main-thread contract, and why does DispatchQueue.main.sync from the main thread crash?
- 10.1.1 What is the difference between serial and concurrent queues and between sync and async, and what do QoS and…
- 10.1.2 How do barriers, groups, semaphores and dispatch sources coordinate work?
- 10.7.1 How do you find and fix deadlocks, data races and slow main-actor hops?
← Blocks are scheduled on the main queue from the main thread, and one… A screen stores a DispatchWorkItem whose closure uses self. Does… →