An app freezes: how do you tell a detected queue deadlock from a real hang, and how do you read a sample or spindump?
Predict first
A serial queue calls sync onto itself through a helper. Does the process freeze?
// === main.swift ===
import Foundation
let q = DispatchQueue(label: "serial")
func load() -> Int { q.sync { 42 } }
q.sync { print(load()) }
print("never")
- No: it traps with status 133
- Yes, forever
- It prints 42
Predict first
Two queues and a semaphore: what does sample show for the thread of queue B?
// === main.swift ===
import Foundation
let a = DispatchQueue(label: "A"), b = DispatchQueue(label: "B")
let s = DispatchSemaphore(value: 0)
a.async { s.wait(); print("A done") }
b.async { a.sync { print("B got A") } }
sleep(1000)
- It waits in _dispatch_sync_f_slow
- It is running user code
- It has exited
Mental model. Two people each holding a door for the other: the camera sees both standing still, and the sample shows which door each is holding.
libdispatch detects the simplest self-deadlock: calling sync on a serial queue you are already running on (even indirectly through a helper) traps immediately with status 133, so you get a crash and a stack instead of a freeze. Deadlocks that cross queues or involve a semaphore, a lock or an actor wait are not detected; the process just stops making progress, and on iOS the watchdog may kill it later (Q228).
To diagnose, take a stack snapshot of every thread: sample <pid> 1 (or Activity Monitor’s Sample Process, spindump for system-wide, or Xcode’s pause and Debug ▸ Pause). In the check, the sample lists three threads: the main thread in sleep, queue A blocked in _dispatch_semaphore_wait_slow at the semaphore line, and queue B blocked in _dispatch_sync_f_slow because it waits for queue A. Read the wait functions to see what each thread waits on, and follow the chain until it loops or reaches something that never signals. Build with -g so frames carry file and line. Fixes: never block a queue on work for a queue that can wait on you, avoid sync from the main thread, and prefer async APIs.
libdispatch detects a same-queue sync and traps.
B is blocked syncing onto A, which is blocked on the semaphore.
Common traps
- Assuming the detector catches every deadlock. Only same-queue sync is trapped.
- Sampling a Release build without symbols. You get addresses; keep the dSYM (Q461).
They might follow up with
- Hang versus slow? Sample twice; changing stacks mean slow.
- Production hangs? MetricKit hang diagnostics (Q462).
Related questions
- 6.6.3 What is the watchdog, and what causes the 0x8badf00d exception?
- 23.5.1 How do you read a crash report, what do the exception types and Swift runtime messages mean, and how does…
- 23.5.2 How do you build a production diagnostics pipeline: collection, symbolication, grouping, prioritisation…
How do the malloc debugging environment variables (such as… →