Home > other >  How to cancel an `async` function with cancellable type returned from `async` operation initiation
How to cancel an `async` function with cancellable type returned from `async` operation initiation

Time:04-17

I need to support cancellation of a function that returns an object that can be cancelled after initiation.

func doSomething() async throws -> ResultData {

    var requestHandle: Handle?
    
    return try await withTaskCancellationHandler {
        requestHandle?.cancel() // COMPILE ERROR: "Reference to captured var 'requestHandle' in concurrently-executing code"
    } operation: {

        return try await withCheckedThrowingContinuation{ continuation in
            
            requestHandle = requester.start() { result, error in
            
                if let error = error
                    continuation.resume(throwing: error)
                } else {
                    let myResultData = ResultData(result)
                    continuation.resume(returning: myResultData)
                }
            }
        }
    }
}

I have reviewed other SO questions and this thread: https://forums.swift.org/t/how-to-use-withtaskcancellationhandler-properly/54341/4

There are cases that are very similar, but not quite the same. This code won't compile because of this error:

"Reference to captured var 'requestHandle' in concurrently-executing code"

I assume the compiler is trying to protect me from using the requestHandle before it's initialized. But I'm not sure how else to work around this problem. The other examples shown in the Swift Forum discussion thread all seem to have a pattern where the requester object can be initialized before calling its start function.

CodePudding user response:

After reviewing the Swift thread again, I see you can do this:

...
var requestHandle: Handle?

let onCancel = { profileHandle?.cancel() }

return try await withTaskCancellationHandler {
    onCancel()
}
...

CodePudding user response:

You said:

I assume the compiler is trying to protect me from using the requestHandle before it’s initialized.

Or, more accurately, it is simply protecting you against a race. You need to synchronize your interaction with your “requester” and that Handle.

But I’m not sure how else to work around this problem. The other examples shown in the Swift Forum discussion thread all seem to have a pattern where the requester object can be initialized before calling its start function.

Yes, that is precisely what you should do. Unfortunately, you haven’t shared where your requester is being initialized or how it was implemented, so it is hard for us to comment on your particular situation.

But the fundamental issue is that you need to synchronize your start and cancel. So if your requester doesn’t already do that, you should wrap it in an object that provides that thread-safe interaction.


For example, let us imagine that you are wrapping a network request. To synchronize your access with this, you can create an actor:

actor ResponseDataRequest {
    private var handle: Handle?

    func start(completion: @Sendable @escaping (Data?, Error?) -> Void) {
        // start it and save handle for cancelation, e.g.,
        
        handle = requestor.start(...)
    }

    func cancel() {
        handle?.cancel()
    }
}

That wraps the starting and canceling of a network request in an actor. Then you can do things like:

func doSomething() async throws -> ResultData {
    let responseDataRequest = ResponseDataRequest()

    return try await withTaskCancellationHandler {
        Task { await responseDataRequest.cancel() }
    } operation: {
        return try await withCheckedThrowingContinuation { continuation in
            Task {
                await responseDataRequest.start { result, error in
                    if let error = error {
                        continuation.resume(throwing: error)
                    } else {
                        let resultData = ResultData(result)
                        continuation.resume(returning: resultData)
                    }
                }
            }
        }
    }
}

You obviously can shift to unsafe continuations when you have verified that everything is working with your checked continuations.

  • Related