Swift: withCheckedContinuation and Dispatch QoSClass

Viewed 162

I am adapting some old code which was using common completions in order to use the new async/await syntax with Parse SDK. Here is an example, this:

static func get(
    className: String,
    id: String,
    _ completion: @escaping (PFObject?) -> Void
) {
    let query = PFQuery(className: className)
    query.getObjectInBackground(withId: id) { object, _ in
        completion(object)
    }
}

is becoming this:

static func get(
    className: String,
    objectId: String
) async -> PFObject? {
    let query = PFQuery(className: className)
    return await withCheckedContinuation { continuation in
        query.getObjectInBackground(withId: objectId) { result, _ in
            continuation.resume(returning: result)
        }
    }
}

However, I was also using DispatchQueue/QoS previously and so the old function actually looked like this:

static func get(
    className: String,
    id: String,
    _ completion: @escaping (PFObject?) -> Void
) {
    let query = PFQuery(className: className)
    DispatchQueue.global(qos: .userInteractive).async {
        query.getObjectInBackground(withId: id) { object, _ in
            DispatchQueue.main.async {
                completion(object)
            }
        }
    }
}

How can I use this with the async/await syntax? Is it even needed?

Thank you for your help

1 Answers

You probably want to execute your async function get(className:objectId:) within a Swift Task, using this Task initialiser:

func foo(
    className: String,
    objectId: String
) async -> PFObject? {
    await Task(priority: .userInitiated) {
        await get(className: className, objectId: objectId)
    }
    .value
}

Note that, when using this Task initialiser you are getting "structured concurrency", which means, that the embedded task in function foo inherits the actor context of the calling function. That is, you can use the result safely from whatever thread you called it.

That means also, if the task where function foo() is running, gets cancelled, the embedded task will be cancelled as well. Of course, cancellation is cooperatively, which means, you need to stop a running task explicitly. For example, as preferred in your use case, with a withTaskCancellationHandler, which calls cancel() on your PFQuery object. Or when you have a long running iterating task, you may poll the Task's cancellation status in reasonably steps while your task progresses.

Please also read about "detached" which behave differently regarding cancellation and inheriting the task priority.

As of your question whether it is needed: I assume, you ask if using specifying a priority is needed: Short answer: in many use cases it may be more safe to just inherit the priority from whatever thread the function originates.

In your case, I would not explicitly change it: when it is called from a background thread with low priority, your task should inheriting this priority as well. There will be no reason to make it high priority with userInitiated. Otherwise, if your function is already called from a user action, it will be inheriting this higher priority as well. I believe, this is what most use cases would require.

Also, when your worker is running in a background thread, as is the case in your code, it really doesn't matter much "how fast you schedule" this task.

So, basically you end up with your async get function as defined, use it as is, and all is good. ;)

Related