Questions about Async/Await

I'm reading Async/Await for Swift. I have several questions:

  1. Is beginAsync only used in non-async functions to create an async context so that one can call other async functions?

  2. How to understand the bold part of this comment about beginAsync:

    Begins an asynchronous coroutine, transferring control to body until it either suspends itself for the first time with suspendAsync or completes, at which point beginAsync returns.

    Can you explain it with an example?

  3. How to implement processImage using async/await?
    Since buttonDidClick in old style looks like this:

    func buttonDidClick(sender: AnyObject) {
        processImage(completionHandler: { image in
            imageView.image = image
        })
    }
    

    I assume processImage in old style should look like this:

    func processImage(completionHandler: @escaping (UIImage) -> Void) {
        DispatchQueue.global().async {
            // Heavy work to prepare the image
            let image = …
            DispatchQueue.main.async {
                completionHandler(image)
            }
        }
    }
    

    How to turn this processImage into async/await style so that it can be used like this:

    func buttonDidClick(sender: AnyObject) {
        beginAsync {
            let image = await processImage()
            imageView.image = image
        }
    }
    

    Like this?

    func processImage() async -> UIImage {
        await DispatchQueue.global().asyncCoroutine()
        // Heavy work to prepare the image
        let image = UIImage()
        await DispatchQueue.main.asyncCoroutine()
        return image
    }
    
  4. What's buttonDidClick's time sequence in async/await style?

    func buttonDidClick(sender: AnyObject) {
        // A. code before `beginAsync`
        beginAsync {
            // B. code before `await`
            let image = await processImage()
            // C. code after `await`
            imageView.image = image
        }
        // D. code after `beginAsync`
    }
    

    A obviously occurs first. But what about the order of B, C, and D? ADBC or ABDC?

async/await is new to me. I still have more questions, but their validity very likely depend on the answers to the questions above. Thanks in advance.

2 Likes

Ideally, with library support, beginAsync would be a primitive operation you rarely encounter or think about yourself, and libraries like libdispatch would provide primitives for working with async contexts. buttonDidClick could then look something like this:

var imageProcessingQueue: DispatchQueue
func buttonDidClick(sender: AnyObject) {
  // use a variation of .async that schedules a () async -> () coroutine
  imageProcessingQueue.asyncCoroutine {
    imageView.image = await processImage()
  }
}

func processImage() async -> UIImage {
  // processing happens on current queue...
}

It's been a while since we last discussed that proposal, and we haven't updated it, but one key bit of feedback we got during our last discussion on it was that coroutines ought to be strongly associated with a specific execution context, such as a dispatch queue, run loop, thread pool, or other event handling mechanism. The manual queue-hopping the proposal draft enables might be clever, but queue hopping is an ongoing correctness and performance problem with libdispatch-heavy code on Apple platforms, and it would likely be very surprising to many users if code could end up running in a different context after an await.

In

func buttonDidClick(sender: AnyObject) {
  imageProcessingQueue.asyncCoroutine {
    imageView.image = await processImage()
  }
}

Shouldn't assigning to imageView.image occur in the main thread?

If that's the case, you'd want to factor it so that the assignment is dispatched on the main queue, and have processImage() perform its processing on another queue. That might look something like this:

func buttonDidClick(sender: AnyObject) {
  // coroutine version of dispatch_async, schedules coroutine without blocking
  DispatchQueue.main.asyncCoroutine {
    imageView.image = await processImage()
  }
}

func processImage() async -> UIImage {
  // coroutine version of dispatch_sync, suspends current coroutine to resume when 
  // sync coroutine returns
  return await imageProcessingQueue.syncCoroutine {
    // expensive processing
    return resultImage
  }
}

But usually we don't consider threading when writing functions like processImage. It is callers's decision to execute it on whatever thread it likes. So the real code in current style would be like this:

func buttonDidClick(sender: AnyObject) {
    imageProcessingQueue.async {
        let image = processImage()
        DispatchQueue.main.async {
            imageView.image = image
        }
    }
}

func processImage() -> UIImage {
    // Heavy work to prepare the image
    return image
}

How would such code look like after converted to async/await?

If you want to make the queue-hopping explicit, then it would end up looking more or less the same. Async/await is ultimately not a great fit for explicit handling of CPU-intensive tasks; it shines more with IO- and event-bound processing. To me, it would be more in the spirit of async to have async interfaces to CPU-bound processes like processImage do the thread or queue management behind the scenes, since that makes the operation look more like an event-driven operation from the main thread's perspective.

That's my understanding too. Feels like a mismatch.

Suppose I already have func processImage() -> UIImage and want to wrap it in an async func so that users can use it nicely in async/await style:

func processImage() async -> UIImage {
  return await imageProcessingQueue.syncCoroutine {
    return processImage()
  }
}

But I guess these two functions cannot coexist, just like a throwing function and a non-throwing function with the same name cannot coexist: compiler won't see them as overloading but redeclaration. Does that mean I have to use something like asyncProcessImage as my wrapper function's name?

Yeah, I don't think we would want to try to do overload resolution based on async-ness, that seems like it would only be confusing.