I want to wrap existing GCD based functions that use both callbacks and callback queues with async/await. I was wondering what pattern I should follow regarding the callback queue. ie:
// This is what I have
func doWork(completeOn queue: DispatchQueue, completion: (Result<Void, Error>) -> Void) { ... }
// I want to wrap this in an async function
func doWork() async throws -> Void {
try await withCheckedThrowingContinuation { continuation in
doWork(completeOn: ???, completion: continuation.resume(with:))
}
}
I don't want to be lazy and use DispatchQueue.main
for the completion queue (and also incur a useless hop to the main queue). What is recommended here? I cannot rewrite the GCD functions.
I'm sure this pattern is frequent enough for a solution to exist, but looking online I could not find much.
First, I assume that the second parameter really is a closure.
func doWork(completeOn queue: DispatchQueue, completion: @escaping (Result<Void, Error>) -> Void) {
…
}
Second, I understand your reticence to use .main
as that might entail a “useless hop to the main queue”. But whatever queue you use, it’s going to entail a useless hop, regardless, because as soon as your async
method returns, it is going to switch back to one of the Swift concurrency cooperative thread pool’s threads, anyway.
But let’s set that aside for a second. And let us assume for a second that you do not have access to the source for doWork
. In that case, you are probably putting this method in an extension. And since extensions cannot add stored properties, I would be inclined to add a type property for this queue:
extension Foo {
static let completionHandlerQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + "Foo.completion") // or `DispatchQueue.global()` or `DispatchQueue.main` or …
func doWork() async throws {
try await withCheckedThrowingContinuation { continuation in
doWork(completeOn: Self.completionHandlerQueue, completion: continuation.resume(with:))
}
}
}
(Obviously, I don’t know what type you are extending, so I used Foo
as a placeholder.)
Above I picked a serial queue because, depending upon what doWork
is doing, you might want to protect yourself against races. And I used a custom queue, to avoid burdening (however modest) the main queue. But use whatever queue is most appropriate. Just know, that regardless of which queue you use, you still have a “useless hop.”
Obviously, if you do have access to the doWork
implementation, it would be ideal to write an async
rendition of doWork
that avoids this DispatchQueue
parameter entirely. That doesn’t necessarily mean changing/breaking the existing GCD interface at all. But you might add a new function that does the async
rendition without the useless dispatch.
FWIW, while you are probably familiar with it, I might refer future readers to WWDC 2021 video Swift concurrency: Update a sample app. It walks you through some of the refactoring alternatives (including some of the Xcode refactoring automation).