--- a/ingest.swift +++ b/ingest.swift @@ -1,38 +1,41 @@ -// EXERCISE 06 — BROKEN STARTING POINT. Do not edit this file; copy it. +// EXERCISE 06 — FIXED VARIANT. The permit moves to the submission site, and a +// structured-concurrency version is measured beside it for comparison. // -// ============================ UNSAFE CODE WARNING ============================ -// This program deliberately blocks many work items on an overcommitting global -// queue. It is bounded — a fixed item count, a fixed block duration, and a -// watchdog — but it will briefly create dozens of kernel threads. Never -// dispatch blocking work this way in a real app. -// ============================================================================= -// // THE SCENARIO -// An importer reads 128 records through a synchronous, blocking API. Someone -// noticed the thread count exploding under Activity Monitor and added a -// DispatchSemaphore "to limit concurrency to 4". The thread count did not -// change. The semaphore is in the code, the limit is right there, and it does -// nothing at all. +// An importer reads 128 records through a synchronous, blocking API, with a +// concurrency limit of 4. // -// WHAT THIS PROGRAM MEASURES -// Live kernel threads in this process, sampled every 10 ms from -// task_threads() — the same number Activity Monitor's "Threads" column shows. -// The reported figure is the high-water mark over the run. +// THE REPAIR — move the admission decision, do not change the primitive +// The semaphore was never the problem and the limit was never wrong. The +// PLACE was wrong. // -// THE MECHANISM TO EXPLAIN -// A Dispatch global queue is OVERCOMMITTING. When a work item blocks, -// libdispatch cannot distinguish "blocked" from "slow", so to keep the -// queue's width occupied it brings up another thread. Blocking work items -// therefore convert directly into threads, each with its own stack and its -// own share of the scheduler. +// Taking the permit inside the work item means all 128 items are already on +// the queue, already started, already holding threads, and only then waiting. +// Taking it before `async` means the SUBMITTING thread blocks instead, and +// work that has not been admitted yet occupies nothing at all. At most +// `limit` items are ever in flight, so at most `limit` threads are ever tied +// up by this workload. // -// Build: swiftc -swift-version 6 -O ingest.swift -o ingest_broken -// Run: ./ingest_broken +// Note the asymmetry that makes this work: the permit is released by the work +// item when it FINISHES, not by the submitter after it submits. A permit +// returned at submission time would bound nothing. // -// Expected: peakThreads several times the core count, and permitHolders never -// above 4 — the permit really is limiting something, just not the thing that -// matters. Exit status 0. +// The second half of this file measures the same bounded workload written +// with a task group. That version is only available if the blocking call can +// become `async`: `Task.sleep` SUSPENDS the task and releases the thread, +// whereas `Thread.sleep` inside a Task would block a cooperative-pool thread +// and is the forward-progress violation to avoid. The cooperative pool is +// deliberately NOT overcommitting — it is sized to the core count — which is +// why blocking inside a Task is a correctness problem rather than a style +// preference. // +// Build: swiftc -swift-version 6 -O ingest.swift -o ingest_fixed +// Run: ./ingest_fixed +// +// Expected: peakThreads close to the core count rather than several times it, +// permitHolders still 4, and a structured-concurrency line showing the same +// bound reached without blocking a thread at all. Exit status 0. +// // Bounded: fixed item count, fixed 120 ms block, and a watchdog that // force-exits after WATCHDOG_S. @@ -105,12 +108,11 @@ let blockMs = 120 let limit = 4 -// THE DEFECT +// THE REPAIR // -// The permit is taken INSIDE the work item. All 128 items are handed to the -// queue immediately; libdispatch starts them; each one then blocks — first on -// the semaphore, later on the work itself. A blocked work item still owns a -// thread, so the pool grows to cover them all. +// The permit is taken BEFORE the work is submitted, so it throttles this one +// submitting thread. Unadmitted work is not on the queue, has not started, and +// holds no thread. The permit is returned by the work item when it completes. // // This function is deliberately NOT async. Swift 6 refuses `DispatchSemaphore // .wait()` in an asynchronous context outright — "unavailable from @@ -126,8 +128,8 @@ let group = DispatchGroup() for _ in 0.. (peak: Int, elapsedMs: Int) { + let meter = ThreadMeter(); meter.start() + let start = Date() + await withTaskGroup(of: Void.self) { group in + var submitted = 0 + for _ in 0..