Search code examples
swiftasync-awaitgrand-central-dispatch

How should I convert a method with a callback queue to async/await?


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.


Solution

  • 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).