SeniorPerformanceRareNot answered yet
A throttle built on the counting primitive DispatchSemaphore causes thread explosion — diagnose and redesign it
Each blocked wait() parks a GCD thread; seeing none idle, GCD spawns more — dozens block on the semaphore (thread explosion), wasting memory. Redesign without blocking — cap concurrency with maxConcurrentOperationCount or async/await.
- ✗Believing a blocked thread costs nothing to keep around
- ✗Thinking a lower semaphore value stops GCD from spawning threads
- ✗Replacing blocking
wait()with blockingsync, which still parks threads
- →Why does GCD create new threads when existing ones are blocked?
- →How does async/await bound concurrency without parking a thread?
The cause is thread blocking. Each semaphore.wait() on the global queue parks a worker thread; GCD, seeing none free, spins up more, and dozens get stuck on the semaphore.
// ❌ before: blocks GCD worker threads
let sema = DispatchSemaphore(value: 4)
for job in jobs {
DispatchQueue.global().async {
sema.wait()
defer { sema.signal() }
job.run()
}
}
// ✅ after: bound concurrency without blocking threads
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 4
jobs.forEach { job in queue.addOperation { job.run() } }
Or async/await: a withTaskGroup capped at 4 concurrent tasks — suspension instead of blocking, so the thread is released while a task waits.