16

I am trying to perform a series of network requests and would like to limit the number of concurrent tasks in the new Swift Concurrency system. With operation queues we would use maxConcurrentOperationCount. In Combine, flatMap(maxPublishers:_:). What is the equivalent in the new Swift Concurrency system?

E.g., it is not terribly relevant, but consider:

func downloadAll() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for index in 0..<20 {
            group.addTask { try await self.download(index) }
        }

        try await group.waitForAll()
    }
}

That results in all the requests running concurrently:

enter image description here

The fact that URLSession is not honoring httpMaximumConnectionsPerHost is interesting, but not the salient issue here. I am looking for, more generally, how to constrain the degree of concurrency in a series of asynchronous tasks running in parallel.

Rob
  • 415,655
  • 72
  • 787
  • 1,044

1 Answers1

25

One can insert a group.next() call inside the loop after reaching a certain count, e.g.:

func downloadAll() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for index in 0..<20 {
            if index >= 6 { try await group.next() }
            group.addTask { try await self.download(index) }
        }

        try await group.waitForAll()
    }
}

That results in no more than six at a time:

enter image description here


For the sake of completeness, I should note that in WWDC 2023 Beyond the basics of structured concurrency, Apple suggests an alternative pattern:

withTaskGroup(of: Something.self) { group in
    for _ in 0 ..< maxConcurrentTasks {
        group.addTask { … }
    }
    while let <partial result> = await group.next() {
        if !shouldStop {
            group.addTask { … }
        }
    }
}

Which, in this example, might translate to:

func downloadAll() async throws {
    try await withThrowingTaskGroup(of: Void.self) { group in
        for index in 0..<6 {
            group.addTask { try await self.download(index) }
        }
        var index = 6
        while try await group.next() != nil {
            if index < 20 {
                group.addTask { [index] in try await self.download(index) }
            }
            index += 1
        }
    }
}

Yielding (in Instruments):

enter image description here

The idea is very similar, namely that you group.addTask {…} up to the max desired concurrency, but then group.next() before adding each subsequent task. It is another way to crack the nut.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • How might I open the network timeline (pictured) from Xcode? (Also, super clean solution, thanks for posting!) – emehex Jun 05 '22 at 23:17
  • @emehex - That happens to be a screen snapshot from [Charles Proxy](https://charlesproxy.com), a wonderful little tool for watching network activity. (It’s also incredibly helpful when diagnosing API traffic, though the initial configuration can admittedly be a little daunting the first time you set it up.) That is, unfortunately, a paid tool, though. You can also use Instruments’ “Points of Interest” tool, which comes as part of Xcode, as discussed [here](https://stackoverflow.com/a/39416673/1271826), though you need to sprinkle `os_signpost` calls before and after the network request. – Rob Jun 05 '22 at 23:30
  • Also, in Xcode’s Instruments, you can use the “Network” template, which includes the “HTTP Traffic” instrument, which will show you a bar graph of how many concurrent network requests you have running across time, which conveys the same basic network activity information, but in a slightly different layout. E.g. here is the Instruments output including “HTTP Traffic” and the “Points of Interest” (obviously having added the `os_signpost` calls to mark the `.begin` and `.end` of each network request). https://i.stack.imgur.com/xR4Ii.png – Rob Jun 06 '22 at 17:29
  • 1
    This may be better as a separate question, but how would you go about adding more tasks once the first set of tasks was already underway? Say in the middle of those 20 operations, another function wants to add 10 more, or something like that? – RL2000 Aug 10 '22 at 20:08
  • 2
    @RL2000 - You can use [`AsyncChannel`](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Channel.md) from [Swift Async Algorithms](https://github.com/apple/swift-async-algorithms). E.g., see example in https://stackoverflow.com/a/73072799/1271826 or https://stackoverflow.com/a/75730483/1271826. – Rob Mar 15 '23 at 00:21