SeniorCodeOccasionalNot answered yet
Run at most N downloads at once with a task group (bounded concurrency)
Download many URLs but keep at most maxConcurrent requests running at any moment, reusing an existing func fetch(_ url: URL) async throws -> Data.
Requirements:
- Never more than
maxConcurrentchild tasks in flight. - Process the entire list; return the results (input order not required).
func download(_ urls: [URL], maxConcurrent: Int) async throws -> [Data] {
// your code here
}
Write the implementation.
Seed the group with the first N tasks; when a result comes back, add the next item. That keeps N in flight — never more than N children — capping memory and connections. Adding every task up front instead launches all at once.
- ✗Assuming a task group self-limits to the core count
- ✗Blocking child tasks with a semaphore instead of throttling additions
- ✗Thinking you cannot control how many group tasks run at once
- →What happens to the remaining queue if one child task throws?
- →How would you preserve input order while capping concurrency at N?
The group holds at most window children: each group.next() frees a slot and you addTask the next URL, so concurrency stays pinned at N:
func download(_ urls: [URL], maxConcurrent: Int) async throws -> [Data] {
try await withThrowingTaskGroup(of: Data.self) { group in
var results: [Data] = []
var next = 0
let window = min(maxConcurrent, urls.count)
for _ in 0..<window { // seed N
let url = urls[next]; next += 1
group.addTask { try await fetch(url) }
}
while let data = try await group.next() { // one out, one in
results.append(data)
if next < urls.count {
let url = urls[next]; next += 1
group.addTask { try await fetch(url) }
}
}
return results
}
}
Because it is a structured group, a thrown error automatically cancels the remaining children. Add all tasks up front and every download starts at once — the bug this pattern avoids.