[Concurrency] Continuations for interfacing async tasks with synchronous code

We's still want to resume regardless, but we can skip unneeded computations (which is the point of cancellation) so we can resume earlier.

I have the same impression that we need something of similar effect.

1 Like

Ah, yes, re-reading the Structured Concurrency proposal document I believe you’re right. So we shouldn’t have the guard !continuation.isTaskCancelled() else { return } line in my example (edited to comment it out). Possibly we don’t need the *Continuation.isTaskCancelled API at all, then? But I do believe we still need something like *Continuation.taskCancellationHandler.

What if you need a more complex series of event? Load this, then load that, etc. If you cancel early, you can skip parts of the computation (within the same continuation) that would otherwise be wasteful.

Then again, it's hard to see what's absolutely required of the glue code. Probably cancellationHandler at the very least to glue together the new Task cancellation and old sync cancellation.

Hello,

I'm wondering about wrapping async processes that are not based on a completion block.

For example, the completion may be provided by an event or a method call. In such cases, one would like to store the continuation somewhere until it is completed:

func operation() async -> OperationResult {
    return await withUnsafeContinuation { continuation in
        OperationImplementation(continuation: continuation).start()
    }
}

class OperationImplementation {
    let continuation: UnsafeContinuation<OperationResult>
    func start() {
        // register for some callback
    }
    func callback(result: OperationResult) {
        continuation.resume(returning: result)
    }
}
  • Is it a pattern that is supported? Fostered?
  • How is the implementation supposed to check for cancellation?
1 Like

+1 for trapping. Possibly this is a behavior that varies with -Ounchecked.

Cancellation seems a bit like throws - some tasks are very interested in it, others not so much. To me this suggests we need withUnsafeCancellableContinuation, withUnsafeThrowawableCancellableContinuation as options, leaving the proposed continuations as noncancellable.

Like in Combine AnyCancellable, some callers will be interested in cancelling when the continuation deinits. I am unsure if CheckedContinuation intends to trap/error on the deinit situation, but this is a case where the correct behavior is different between cancellable/noncancellable, which is another reason for having more types.

There is a natural conversion between types, namely that cancellable tasks are a bit like a failure and vice versa. Callers that are uninterested in distinguishing between a task that was cancelled and one that failed ought to be able to interchange these, as to write one branch for success and the other for not-success without having to think in detail about all the reasons the task may no longer be running.

This seems fine (supported), you do want to ensure to not call start() twice though as that’d resume twice which is bad™. Other than that such patterns are fine.

Thank you @ktoso! And what about cancellation checking?

To be honest I find it a bit weird to want this specific API have anything to do with cancellation — it is for contexts which are not within the task infrastructure so by itself it can’t do anything about that.

In [Pitch #2] Structured Concurrency I propose additions or reshaping of the Task APIs such that one would be able to get a Task object, and if one wanted to one could then check it from wherever — including implementations using *Continuations. That’s about as much as we can achieve I think.

3 Likes

@ktoso, do we have to expect developers to wrap many existing asynchronous APIs that support cancellation, such as completion blocks, Combine publishers, other reactive tools, DispatchQueue/thread/mutex/lock-based APIs, etc? Maybe those APIs will eventually be migrated to Swift's async model. But some will not. And this eventual migration will take time.

Meanwhile, do we really want to ignore cancellation?

I think that querying cancellation & local data make sense semantically from the UnsafeContinuation object, at least up until the resume is called. We could treat everything until resume to be part of the task since the task is suspended until then. Task local data would be stable, and cancel are synchronized anyway. After resume, these could just trap.

While I don't see much problem in omitting task local (we can use capture list as others pointed out), we probably need cancellation, since that's the only ever-changing thing that's queried inside a task.

Based on the current structured concurrency pitch, I would expect this to be written as follows, no continuation-specific cancellation functionality needed:

func download(url: URL) async throws -> Data? {
    var urlSessionTask: URLSessionTask?

    return try Task.withCancellationHandler { urlSessionTask?.cancel() }
    operation: {
        return try await withUnsafeThrowingContinuation { continuation in
            urlSessionTask = URLSession.shared.dataTask(with: url) { data, _, error in
                if let error = error {
                    // Ideally translate NSURLErrorCancelled to CancellationError here
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: data)
                }
            }
            urlSessionTask?.resume()
        }
    }
}
1 Like

Nothing is being ignored, it has nothing to do with this specific API, so there's no reason to cram in more complexity into it.

Through composition with APIs proposed in structured concurrency, it is able to handle cancelation when necessary, exactly like @jayton suggests (that's one of the ways):

Note that since you're in an async function already (in download), you can normally Task.isCanceled check before submitting any work as well.

Please refer to [Pitch #2] Structured Concurrency for more details on structured concurrency and those APIs; If we made Task a value, then the insides of the continuation can check for it: task.isCanceled as well.

3 Likes

Thanks for this clear answer. Questions are not there to create difficulties, but to get answers to the expected use cases that are not covered in the proposal text. :+1:

2 Likes

No worries, thanks for the feedback -- we're adding addressing those questions to the proposal here: https://github.com/apple/swift-evolution/pull/1244 :+1:

1 Like

I've updated the proposal to propose trapping on multiple resumes, and added some examples and discussion from this thread. Thanks everyone.

8 Likes

How about switching checked or unchecked automatically depending on if -Ounchecked (or maybe -O) flag is on? It seems consistent with precondition (or assert).

1 Like

I don't think we can do that automatically because the types are not layout-compatible; CheckedContinuation needs to hold on to a class instance whose lifetime tracks whether the continuation is leaked, whereas UnsafeContinuation can be implemented as a raw pointer to the task structure.

2 Likes

I got it. Thank you.

Please note that this proposal has now entered the review phase: SE-0300: Continuations for interfacing async tasks with synchronous code :eyes:

2 Likes
Terms of Service

Privacy Policy

Cookie Policy