Wow! Does SE-0420 really make the guarantee that no suspension will ever happen before operation
is executed? I missed that!
Thatβs just a property of isolated functions. The linked proposal makes it possible to write the optional actor isolation parameter making such implementation method possible. Iβm not sure about the shape of the proposed method but yeah something like that could come in handy.
I can't speak to the suspension point guarantees but SE-0313 Actor Isolation Control has this choice quote:
This makes instance methods on actor types less special, because now they are expressible in terms of a general feature: they are methods for which the
self
parameter isisolated
Therefore we can achieve this in recent Swift releases with:
func withCancellingContinuation<T>(
isolation: isolated some Actor,
operation: (CheckedContinuation<T, Error>) -> Void,
onCancel handler: @Sendable () -> Void
) async throws -> T {
We need to jump through hoops to extract an isolated
reference to a Global actor like this:
extension Actor {
func performIsolated<T>(_ closure: (isolated Self) async throws -> T) async throws -> T {
return try await closure(self)
}
}
try await MainActor.shared.performIsolated { isolation in
try await withCancellingContinuation(isolation: isolation) {
MainActor.shared.assertIsolated()
$0.resume()
} onCancel: {
print("π")
}
}
SE-0420 makes it possible to:
- express a function is dynamically non-isolated with
nil
- makes it easy to form a reference to the current isolation via
#isolation
.
On Actors and caching;
I like implementing these algorithms lock free with Actors but this thread along with How to use withTaskCancellationHandler properly? point to some of the issues.
It can help to associate cancellation with a stable identifier β SE-0392 and Actor.assertIsolated
(thanks @ktoso) gave me confidence to generalise cancellation which IMO really improves the API when used with Actors:
await withIdentifiableContinuation(isolation: self) { (continuation, id) in
addContinuation(continuation, with: id)
} onCancel: { id in
Task { await self.cancelContinuation(with: id) }
}
The continuation can be immediately appended to the actors isolated state and work commences. Cancellation is nonisolated so must be enqueued on the actor.
If only CheckedContinuation
was Identifiable
....
I do want to stress that without these guarantees, this function won't actually work the same. Both withTaskCancellationHandler
and withCheckedThrowingContinuation
make use of @_unsafeInheritExecutor
and that can be essential for use within actors.
Thankfully, SE-0420 does make this work, which I think is awesome!