Hello,
I can't figure out how to trigger the cancellation of an async sequence from a break
in an async for
loop. Am I holding it wrong, or should I report an issue?
Some async sequences have to perform clean up when iteration has stopped. I'm interested in making sure this clean up is performed when I break an async for
loop.
In the example below, I'll use an async sequence built from a Combine publisher that logs the cancellation of its subscriptions, because it was the quickest way for me to produce a sample code.
This is my async sequence below. It prints CANCEL when it is cancelled:
import Combine
import Foundation
// Forever produces a Date every second
let dates = Timer
.publish(every: 1, tolerance: 0, on: .main, in: .default)
.autoconnect()
.handleEvents(receiveCancel: { print("CANCEL") })
.values
Next, I define an async function that loops over this sequence, and breaks after three iterations:
func brokenLoop() async {
var counter = 0
for await date in dates {
print(date)
counter += 1
if counter == 3 { break }
}
print("END")
}
Finally, I want to see the cancellation in action, so I setup a tiny app:
// First app prints:
// 2021-10-13 16:40:54 +0000
// 2021-10-13 16:40:55 +0000
// 2021-10-13 16:40:56 +0000
// END
@main
struct AsyncApp {
static func main() async {
do {
let task = Task {
await brokenLoop()
}
await task.value
}
// Give time to the runtime
await Task.sleep(10 * 1_000_000_000)
}
}
I can see three dates, and the END word, but never CANCEL
However, I can see CANCEL when I cancel the task that runs the brokenLoop
function:
// Second app prints:
// 2021-10-13 16:40:53 +0000
// CANCEL
// END
@main
struct AsyncApp {
static func main() async {
do {
let task = Task {
await brokenLoop()
}
await Task.sleep(1_500_000_000)
task.cancel()
}
// Give time to the runtime
await Task.sleep(10 * 1_000_000_000)
}
}
Because I can see CANCEL when the parent task is cancelled in the second app, I believe the async sequence correctly handles the cancellation flag.
Because I can not see CANCEL when the loop is broken and the parent task proceeds to completion, in the first app, I start having doubts about how break
is handled.
When I ask Xcode for a memory snapshot, my lack of experience prevents me from understanding who is retaining the subscription (see screenshot). I can see something that looks like attached to an async iterator, but I don't understand who keeps the strong reference on it (and shouldn't):
Do you have an idea about what's wrong?