Structured caching in an actor

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.

1 Like

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 is isolated

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.
1 Like

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....

1 Like

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!

1 Like