withThrowingTaskGroup - (un)expected progress behavior?

Dear community,

I'm trying to use the new structured concurrency features of Swift 5.5 to update some code in a larger App that has been written using GCD. One routine is loading parts of a dataset in a parallel loop and I would like to convert this code using withThrowingTaskGroup while keeping a progress counter. The code compiles, runs, but does not update the progress in a way I expected:

var progressCounter = 0
let count = 500

try await withThrowingTaskGroup(of: Void.self) { group in
  for idx in 0..<count {
    _ = group.addTaskUnlessCancelled {
      // some IO
    }
  }
  for try await _ in group {
    progressCounter += 1
  }
}

The for try await loop is executed after all 500 task have been run and finished (iOS 15 Beta 5). I was under the impression, the for try await loop would be run in parallel to the tasks, collecting results as they come in and thereby updating the progress counter. Clearly I'm missing something or is this expected? Is withThrowingTaskGroup not the right tool for this?

1 Like

I suspect that there’s some hidden serialisation in the tasks that you add to the group. Consider this:

func test() async throws {
    var progressCounter = 0
    let count = 100
    
    try await withThrowingTaskGroup(of: Void.self) { group in
        for idx in 0..<count {
            _ = group.addTaskUnlessCancelled {
                try await Task.sleep(nanoseconds: UInt64(idx) * 200_000_000)
            }
        }
        let start = Date.now
        for try await _ in group {
            progressCounter += 1
            let t = String(format: "+%07.3f", Date.now.timeIntervalSince(start))
            print("\(t) progress")
        }
        print("done")
    }
}

When I ran this here (Xcode 13.0b5 targeting the iOS 15 simulator) I saw the results dribble in over time:

+000.001 progress
+000.207 progress
+000.439 progress
+000.651 progress
+000.876 progress
+001.087 progress
…
+018.754 progress
+019.601 progress
+019.601 progress
+019.601 progress
+019.601 progress
+019.601 progress
+020.078 progress
done

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Thank you Eskimo for this piece of code, it helped me to figure out my mistake (or wrong assumption). My tasks all run very much the same time. The old code used a semaphore to only ever let 4 tasks to run in parallel on iOS, which was fine. I was under the wrong assumption, the new task group concurrency would internally limit the number of tasks that are launched/scheduled in parallel (do some kind of queuing), which doesn't seem the case, thus all 500 (or in your case 100) tasks get scheduled more or less at the same time and in my case also finish roughly at the same time.