I found that the Task
was being suspended even if the closure of the Task
had been done.
So I make a test code like below.
struct ContentView: View {
let testClass = TestClass()
var body: some View {
Button(action: {
testClass.start()
}, label: {
Text("button")
})
}
}
class TestClass {
var task: Task<Void, Never>?
func start() {
task?.cancel()
task = Task {
try? await Task.sleep(nanoseconds: 5_000_000_000)
}
}
}
And instrument is showing like below.
My assumption was that the task will have a status like finished, because the closure was done already. But it's not. It's showing suspended and still alive.
Is this a right concept or I made a wrong example?
I want to get a reference of the task for cancel, and don't want to see a count on Alive Tasks. If I add a code like task = nil
after sleep
, it would work as I expected.
Could it be a right workaround?
If you save the Task
in a property, the underlying task still exists (even if it finished running its closure), including the task state (i.e., its value
or result
).
Now, in your example, the Task
returns a Void
, so it is hard to see this behavior. But, imagine a Task
that returned a value or throws
errors. You can still fetch [try] await task?.value
even after the Task
is done, and you will see the result of the Task
closure.
Consider this variation, where we have a separate button that verifies the result of the Task
:
import SwiftUI
import os.log
struct ContentView: View {
let experiment = Experiment()
var body: some View {
VStack {
Button {
Task { await experiment.start() }
} label: {
Text("start")
}
Button {
Task { await experiment.verify() }
} label: {
Text("verify")
}
}
.padding()
}
}
actor Experiment {
var task: Task<Int, Error>?
let signposter = OSSignposter(subsystem: "Experiment", category: .pointsOfInterest)
func start() async {
signposter.emitEvent(#function, "start")
let state = signposter.beginInterval(#function, id: signposter.makeSignpostID())
task?.cancel()
let task = Task {
try await Task.sleep(for: .seconds(5))
return Int.random(in: 0..<100)
}
self.task = task
do {
let result = try await task.value
signposter.endInterval(#function, state, "result = \(result)")
} catch {
signposter.endInterval(#function, state, "error = \(error)")
}
// Really, we should wrap the above `do`-`catch` in a cancellation handler,
// as we’re responsible for that when introducing unstructured concurrency,
// but I was trying to keep it simple. But, you’d probably do something like:
//
// await withTaskCancellationHandler {
// do {
// let result = try await task.value
// …
// } catch {
// …
// }
// } onCancel: {
// task.cancel()
// }
}
func verify() async {
signposter.emitEvent(#function, "start")
let state = signposter.beginInterval(#function, id: signposter.makeSignpostID())
guard let task else {
signposter.endInterval(#function, state, "no task")
return
}
do {
let result = try await task.value
signposter.endInterval(#function, state, "result = \(result)")
} catch {
signposter.endInterval(#function, state, "error = \(error)")
}
}
}
If I profile that (with not only the “Swift Tasks” tool, but also the “Points of Interest” tool that captures my OSSignposter
events) and first tap “start” (the first ⓢ signpost) and then tap “verify” before start finishes (the second ⓢ), we see:
So, we see that as soon as the “start” task finishes, the “verify” task does, too. And the “verify” task shows the same result of the Task
as we saw in the “start” task. This is interesting, but perhaps unsurprising.
But, if we instead defer the tap on “verify” until the “start” task finishes, we still see the “verify” task show the result for the “finished” task:
Now in this case, the “verify” task runs nearly instantaneously (because it does not have to wait for the “start” Task
to finish), but, still, although the “start” Task
was already “finished”, the “verify” routine still returns the value of the saved Task
.
But, I agree with you that referring to a finished Task
as “suspended“ because it still exists, is arguably a curious choice of terminology. But you can also see why it might still be considered “alive”, as it still exists and you can still fetch its value
(and or determine if it was cancelled or threw other error) even though its closure has finished execution.
You said:
I want to get a reference of the task for cancel, and don't want to see a count on Alive Tasks. If I add a code like
task = nil
aftersleep
, it would work as I expected.Could it be a right workaround?
Yes, that is a fine solution if you really do not need the task, anymore.
For what it is worth, the amount of memory consumed by this Task
that has finished execution is fairly minimal. Any locals or variables captured by the closure are released: It is largely just the task state and its results that are saved once execution finishes.