What are thread explosion and priority inversion, and how do QoS and the system help?

10.1.3 · L5 · Interview

Predict first

Hundreds of blocks on a concurrent queue each call sync on a serial queue and wait. What is the most likely symptom?

// A: nothing, the system limits the threads
// B: many threads are created, most of them blocked, and the app can stall
// C: the blocks run in submission order
  • A
  • B
  • C

Mental model. Thread explosion is hiring a new clerk every time one is stuck waiting, until the building is full of idle clerks. Priority inversion is the VIP stuck behind a slow customer who is stuck behind a crowd.

Thread explosion happens when you submit many blocks to a concurrent queue and those blocks block (waiting on I/O, a lock or a sync call). GCD sees idle CPU cores and no progress, so it starts more threads to run the next blocks, and can reach the system’s thread limit while all of them wait. Costs: memory for each thread’s stack, scheduling overhead, and potential deadlock when every worker thread is waiting for work that needs a free one. The fixes are to not block (use async APIs and callbacks), to serialise work on a serial queue or limit it with an OperationQueue’s maxConcurrentOperationCount or a semaphore, and to avoid nested sync calls. Swift concurrency addresses this by running tasks on a cooperative thread pool with about one thread per core: tasks must not block threads, they suspend at await, which is why blocking calls in async code are flagged.

Priority inversion is a high-priority task waiting on a resource held by a lower-priority one, which in turn is starved by medium-priority work: the urgent task is effectively running at the lowest priority. Apple’s libraries cope in two ways: QoS inheritance and promotion (a queue or thread blocked on by a higher-QoS caller is temporarily raised, and sync and locks like os_unfair_lock, DispatchQueue.sync and pthread mutexes donate priority) and QoS-aware primitives. Things that do not propagate priority, such as a semaphore or a spin loop, can cause inversion, which the Thread Performance Checker flags in Xcode (“priority inversion” warnings). Use the lowest QoS that fits, never wait on a low-QoS queue from a high-QoS one, and prefer locks and queues that donate priority.

Blocking blocks look like idle capacity to GCD, so it keeps adding threads until it reaches its limit, which is thread explosion.

Common traps

  • Blocking a worker thread on a semaphore or sync inside concurrent work. It triggers more threads and risks starvation.
  • Waiting on a background-QoS queue from the main thread. The user-interactive work is held back by background work.

They might follow up with

  • How does Swift concurrency avoid thread explosion? A fixed-size cooperative pool; tasks suspend at await instead of blocking.
  • How do you catch priority inversion? The Thread Performance Checker in Xcode, and Instruments’ System Trace.

Related questions

  • 10.1.1 What is the difference between serial and concurrent queues and between sync and async, and what do QoS and…
  • 10.3.1 What does async/await actually do, and what is a suspension point?
  • 10.7.1 How do you find and fix deadlocks, data races and slow main-actor hops?

← How do barriers, groups, semaphores and dispatch sources coordinate…