Hello,
I've noticed that TaskGroup has poor performance as the number of subtasks grow.
I've created sample SwiftUI app ( see below ) to demonstrate the issue.
If you run the code ( with release mode & optimisations enabled ) and increase the number of operations, you will notice that all functions that rely on TaskGroup are getting slower and then completely stuck, while simple loop with async calls or DispatchGroup approach runs under a second.
There were other threads, where it was mentioned that using TaskGroup should be better since it might know number of tasks and schedule better.
Is this expected behaviour?
Sample App
import SwiftUI
struct ContentView: View {
@State var number: String = "100"
var body: some View {
VStack {
TextField("Number of operations", text: $number)
Button("Actors TaskGroup", action: {
Task.detached(priority: .high) {
await runTaskGroup(name: "Actors TaskGroup", counters: Factory.actors(numberOfIterations()))
}
})
Button("Actors Simple Loop", action: {
Task.detached(priority: .high) {
await runSimpleLoop(name: "Actors Simple Loop", counters: Factory.actors(numberOfIterations()))
}
})
Button("Queues TaskGroup", action: {
Task.detached(priority: .high) {
await runTaskGroup(name: "Queues TaskGroup", counters: Factory.queues(numberOfIterations()))
}
})
Button("Queues Simple Loop", action: {
Task.detached(priority: .high) {
await runSimpleLoop(name: "Queues Simple Loop", counters: Factory.queues(numberOfIterations()))
}
})
Button("Queues Dispatch-Group", action: {
let queues = Factory.queues(numberOfIterations())
let dispatcher = DispatchQueue(label: "Queues Dispatch-Group", qos: .userInitiated)
let start = CFAbsoluteTimeGetCurrent()
dispatcher.async {
let incrementGroup = DispatchGroup()
for queue in queues{
incrementGroup.enter()
queue.increase(completion: { incrementGroup.leave() })
}
incrementGroup.notify(queue: dispatcher) {
let readGroup = DispatchGroup()
var result: [Int] = []
for queue in queues {
readGroup.enter()
queue.read(completion: { value in
dispatcher.async {
result.append(value)
readGroup.leave()
}
})
}
readGroup.notify(queue: dispatcher) {
let end = CFAbsoluteTimeGetCurrent()
precondition(result.allSatisfy({ $0 == 1 }))
print("Queues Dispatch-Group iterations: \(queues.count), took: \(end - start)")
}
}
}
})
}
}
private func numberOfIterations() -> Int {
return Int(number) ?? 1
}
}
protocol Counter {
func increase() async
func read() async -> Int
}
actor CounterActor: Sendable, Counter {
private var value: Int = 0
func increase() async { value += 1 }
func read() async -> Int { value }
}
final class CounterQueue: Counter, @unchecked Sendable {
private var value: Int = 0
private let queue = DispatchQueue(label: "CQ", qos: .userInteractive)
func increase(completion: @escaping () -> Void) {
queue.async {
self.value += 1
completion()
}
}
func increase() async {
await withCheckedContinuation({ continuation in
self.increase(completion: { continuation.resume() })
})
}
func read(completion: @escaping (Int) -> Void) {
queue.async { completion(self.value) }
}
func read() async -> Int {
await withCheckedContinuation({ continuation in
self.read(completion: { continuation.resume(returning: $0) })
})
}
}
struct Factory {
static func actors(_ numberOfEntries: Int) -> [CounterActor] {
var result: [CounterActor] = []
result.reserveCapacity(numberOfEntries)
for _ in 1...numberOfEntries {
result.append(CounterActor())
}
return result
}
static func queues(_ numberOfEntries: Int) -> [CounterQueue] {
var result: [CounterQueue] = []
result.reserveCapacity(numberOfEntries)
for _ in 1...numberOfEntries {
result.append(CounterQueue())
}
return result
}
}
func runTaskGroup<C: Counter>(name: String, counters: [C]) async {
let start = CFAbsoluteTimeGetCurrent()
await withTaskGroup(of: Void.self, body: { group in
for counter in counters { group.addTask { await counter.increase() } }
for await _ in group { }
})
await withTaskGroup(of: Int.self, returning: Void.self, body: { group in
for counter in counters { group.addTask { await counter.read() } }
var result: [Int] = []
for await value in group {
result.append(value)
}
let end = CFAbsoluteTimeGetCurrent()
precondition(result.allSatisfy({ $0 == 1 }))
print("\(name) Iterations: \(counters.count), took: \(end - start)")
})
}
func runSimpleLoop<C: Counter>(name: String, counters: [C]) async {
let start = CFAbsoluteTimeGetCurrent()
for counter in counters {
await counter.increase()
}
var result: [Int] = []
for counter in counters {
let value = await counter.read()
result.append(value)
}
let end = CFAbsoluteTimeGetCurrent()
precondition(result.allSatisfy({ $0 == 1 }))
print("\(name) Iterations: \(counters.count), took: \(end - start)")
}