Using discardingTaskGroup to limit concurrent tasks?

I'm having trouble understanding the use case for discardingTaskGroup. In my app, I want to submit 10 concurrent image upload requests; usually, I'd just fire off 10 unstructured Task {} instances (I'm assuming this is fine)

for image in images {
    Task {
        do {
            try await uploadImage(item: item,
                                  image: image)
        } catch {
            // Handle any errors
        }
    }
}

But then I thought I'd actually like to have a max of ~3 uploads concurrently, where I would prioritize the images that appear to the user earlier first. I know using group.next() in a taskGroup we can await on previous results and add tasks as required.

But my task does not return data, rather it performs an action. So, it seems like the new discardingTaskGroup could be a useful API.

Task {
    do {
        try await withThrowingDiscardingTaskGroup { group in
            for image in images {
                group.addTask {
                    try await uploadImage(item: item,
                                          image: image)
                }
            }
        }
    } catch {
        // Handle any errors
    }
}

How can I convert this discarding task group code to only include a max of n tasks running concurrently? And is this even a reasonable use of the new API to begin with?

Best, T

Accepted Reply

The best way to understand a discarding task group is to contrast it against a standard one. In a standard task group the group accumulates all of the results of all of the tasks. This is problematic if the code within the group runs forever.

Imagine you’re building a network server using structured concurrency. The top-level task runs the listener, and it runs forever. You don’t want to accumulate its results in the task group, because that’ll lead to unbounded memory growth.

You can learn more about this in SE-0381 DiscardingTaskGroups.

How can I convert this discarding task group code to only include a max of n tasks running concurrently?

Limiting the number of tasks running concurrently within a group isn’t a feature of discarding task groups, or indeed standard task groups. It’s possible to do this, but you have to jump through some hoops. You might do something like this:

let downloads: [ImageDownloadDetails] = …
await withTaskGroup(of: Void.self) { group in
    var remaining = downloads[...]
    for _ in 0..<3 {
        guard let details = remaining.popFirst() else { break }
        group.addTask {
            … download item based on `details` …
        }
    }

    while let details = remaining.popFirst(), let _ = await group.next() {
        group.addTask {
            … download item based on `details` …
        }
    }
}

The for loop starts the first 3 tasks. Once that’s done, it falls through to the while loop, which waits for a running task to complete before starting the next one.

IMPORTANT I constructed this from various examples I found lying around. It compiles, but I can’t claim to have actually tested it (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Thanks for your response! So, my understanding is that there is no longer a use case for using the old TaskGroup API with a type of Void.self. After all, if there is no result returned, you might as well not allocate space for it in memory and use this discardingTaskGroup API instead.

Add a Comment

Replies

The best way to understand a discarding task group is to contrast it against a standard one. In a standard task group the group accumulates all of the results of all of the tasks. This is problematic if the code within the group runs forever.

Imagine you’re building a network server using structured concurrency. The top-level task runs the listener, and it runs forever. You don’t want to accumulate its results in the task group, because that’ll lead to unbounded memory growth.

You can learn more about this in SE-0381 DiscardingTaskGroups.

How can I convert this discarding task group code to only include a max of n tasks running concurrently?

Limiting the number of tasks running concurrently within a group isn’t a feature of discarding task groups, or indeed standard task groups. It’s possible to do this, but you have to jump through some hoops. You might do something like this:

let downloads: [ImageDownloadDetails] = …
await withTaskGroup(of: Void.self) { group in
    var remaining = downloads[...]
    for _ in 0..<3 {
        guard let details = remaining.popFirst() else { break }
        group.addTask {
            … download item based on `details` …
        }
    }

    while let details = remaining.popFirst(), let _ = await group.next() {
        group.addTask {
            … download item based on `details` …
        }
    }
}

The for loop starts the first 3 tasks. Once that’s done, it falls through to the while loop, which waits for a running task to complete before starting the next one.

IMPORTANT I constructed this from various examples I found lying around. It compiles, but I can’t claim to have actually tested it (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • Thanks for your response! So, my understanding is that there is no longer a use case for using the old TaskGroup API with a type of Void.self. After all, if there is no result returned, you might as well not allocate space for it in memory and use this discardingTaskGroup API instead.

Add a Comment