I have the following polling task. Its roughly exactly like this but obviously a bit contrived to make the question simple.
class SocketClass {
private var pollingTask: Task<Void, Never>?
func startPoll() {
// Stop previous polling task
pollingTask?.cancel()
pollingTask = nil
pollingTask = Task {
while !Task.isCancelled {
// Send ping
// Wait for pong timeout
await self.waitForPong()
// Wait for the next ping interval
try? await Task.sleep(nanoseconds: UInt64(30_000_000_000))
}
}
// Is it possible that at this point that there are two tasks
// attempting to poll before the previous one heard it needed to cancel?
}
private func waitForPong(_ uuid: UUID, _ count: Int) async -> Bool {
let pongReceived = await withCheckedContinuation { continuation in
socket.once("pong") { [weak self] _, _ in
continuation.resume(returning: true)
}
}
return pongReceived
}
}
My concern is when startPoll() is called that the previous polling task may not be canceled by the time the new one starts. I would feel safer if I could await the cancelation of the previous task but not quite sure how to do that.
But with the code as is there are no guarantees that there might be a moment where there are two polling tasks working simultaneously for at least a little bit right? Especially if the ping/pong code doesnt check if the task is canceled.
2
Answers
You could change your task to
throw
when it is cancelled by callingcheckCancellation
instead of checkingisCancelled
. You can thenawait pollingTask.result
– Once that await completes you know that the task has stopped:Your assumption is correct, that there can be a race of the cancellation and starting the poll, which makes it possible, that two operations run at the same time. A solution requires that the start of the operation should only begin, when the previous one (if any) is completed.
The following class solves this issue. Note, that there are other solutions possible.
Note also, that the class is running on the MainActor to make it thread-safe. The function
start()
cannot overlap when called from multiple clients, howeverstart()
can be called, when the internal operation is still running. In this case, the current task is cancelled, then its completion is awaited and only then, the new task will be created and started.You can visualise and test this class when making it an Observable and show the state in a View:
and a SwiftUI view: