withTaskCancellationHandler "This function returns instantly and will never suspend."

withTaskCancellationHandler is documented with "This function returns instantly and will never suspend."

Since it returns the value of its async operation closure, how does it accomplish that?

1 Like

I think what the authors meant by that comment is that by itself the withTaskCancellationHandler method returns instantly and will by itself never suspend, i.e. the following code returns instantly and never suspends

func foo() async {
    try await withTaskCancellationHandler {
        // empty body
    } onCancel: { }
}

However, I understand where your confusion comes from. You are right that if you are calling another async method in the operation closure which is suspending that the call to withTaskCancellationHandler will suspend.

1 Like

In your example, would Swift definitely avoid suspending? Since we don't have something like reasync, from the perspective of withTaskCancellationHandler, isn't invoking operation always a potential suspension point?

In my example there is guaranteed no suspension happening. The await keyword marks a potential suspension point; however, the actual suspension only happens if with[Checked/Unsafe]Continuation is called.
This also allows some nice performance optimisation if the compiler can prove that the no suspension happens.

1 Like

What if foo() is actor-isolated? In light of SE-0338, would there have to be a suspension?

No, even in that case there won't be any suspension inside foo; however, there might be a suspension before foo is getting called since the actor might be busy doing other things.

So here's the implementation: swift/TaskCancellation.swift at swift-5.7.1-RELEASE · apple/swift · GitHub

withTaskCancellationHandler is itself a non-actor-isolated async function. So, isn't it guaranteed not to run on any actor's executor? And if it's invoked by an actor, then wouldn't/couldn't the async function that it was called from be suspended, regardless of what the operation closure does?

/// redefinition (not a full implementation, but sufficient for purposes of this demonstration)
func withTaskCancellationHandler<T>(
    operation: () async throws -> T,
    onCancel handler: @Sendable () -> Void
) async rethrows -> T {
    // not on any actor
    print("B")
    return try await operation()
}

@MainActor final class Something {
    func doSomething() async {
        print("A")
        await withTaskCancellationHandler {
            // on main actor
            print("C")
        } onCancel: { }
    }
}

I ran Something.doSomething() in an iOS app and set a breakpoint on print("B"). When the breakpoint is hit, it's not on the main thread, but print("A") and print("C") are on the main thread. So unless the std lib's withTaskCancellationHandler gets special treatment that my redefinition does not, it seems like it does suspend. (If there is special treatment, I'm curious to learn where/how that's implemented.)

So we have to differentiate between two things here. First, if withTaskCancellationHandler itself suspends and secondly if the call to withTaskCancellationHandler needs to actor hop. The former is stated in the docs and withTaskCancellationHandler itself never suspends. That means when it runs it will execute the passed operation right away. The latter is more interesting and you are right this might needs to actor hop which can suspend. The more interesting part here is the actual actor isolation.

The following code compiled with -warn-concurrency produces an actual warning.

actor Foo {
    func bar() async {
        await withTaskCancellationHandler {
            // Non-sendable type '() async throws -> ()' exiting actor-isolated context in call to non-isolated global function 'withTaskCancellationHandler(operation:onCancel:)' cannot cross actor boundary
        } onCancel: { }
    }
}

This warning is something that @John_McCall or @Douglas_Gregor are probably better suited to comment on if this should stay or how cancellation handlers ought to work in actors.