How can I convert to Swift async/await from GCD (DispatchQueue)?
Asked Answered
W

1

28

I am following Stanfords' CS193p Developing Apps for iOS online course.

It is using the Grand Central Dispatch (GCD) API for a demo of multithreading. But they noted, that

"GCD has been mostly replaced by Swift's new built-in async API as of WWDC 2021".

So I wanted to learn how the code from the Lecture would look like after updating it to use this new API.

After watching Apple's WWDC videos, it seems to me like
DispatchQueue.global(qos: .userInitiated).async { } is replaced in this new async API with Task { } or Task(priority: .userInitiated) {}, but I'm not sure, what has DispatchQueue.main.async { } been replaced with?

So, my questions are:

  1. Am I correctly assuming, that DispatchQueue.global(qos: .userInitiated).async { } has been replaced with Task(priority: .userInitiated) {}
  2. What has DispatchQueue.main.async { } been replaced with?

Please help, I want to learn this new async-await API.

Here's the code from the Lecture, using old GCD API:

DispatchQueue.global(qos: .userInitiated).async {
    let imageData = try? Data(contentsOf: url)
    DispatchQueue.main.async { [weak self] in
        if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
            self?.backgroundImageFetchStatus = .idle
            if imageData != nil {
                self?.backgroundImage = UIImage(data: imageData!)
            }
            // L12 note failure if we couldn't load background image
            if self?.backgroundImage == nil {
                self?.backgroundImageFetchStatus = .failed(url)
            }
        }
    }
}

The whole function (in case you need to see more code):

private func fetchBackgroundImageDataIfNecessary() {
    backgroundImage = nil
    switch emojiArt.background {
    case .url(let url):
        // fetch the url
        backgroundImageFetchStatus = .fetching
        DispatchQueue.global(qos: .userInitiated).async {
            let imageData = try? Data(contentsOf: url)
            DispatchQueue.main.async { [weak self] in
                if self?.emojiArt.background == EmojiArtModel.Background.url(url) {
                    self?.backgroundImageFetchStatus = .idle
                    if imageData != nil {
                        self?.backgroundImage = UIImage(data: imageData!)
                    }
                    // L12 note failure if we couldn't load background image
                    if self?.backgroundImage == nil {
                        self?.backgroundImageFetchStatus = .failed(url)
                    }
                }
            }
        }
    case .imageData(let data):
        backgroundImage = UIImage(data: data)
    case .blank:
        break
    }
}
Worms answered 21/11, 2021 at 17:10 Comment(9)
You likely shouldn't have been using .userInteractive for Data(contentsOf: url) in your original code. Instead, look at the asynchronous URL methods like using URLSession. hackingwithswift.com/books/ios-swiftui/…Fdic
Or leave async/await for now if you are a beginner programmer. Besides, you are asking us to do all the work for you here which isn’t how it is supposed to be.Stemma
You are right, it was .userInitiated, not .userInteractive in the Lecture. I will update the question.Worms
And you are right, that using URLSession' is a better way to do this, but the Stanford professor was temporarily using GCD instead to **explain** multithreading. A few lectures later he changed that to URLSession`Worms
And I am not asking you to do all the work. I am asking: 1. Am I correctly assuming, that DispatchQueue.global(qos: .userInitiated).async { } has been replaced with Task(priority: .userInitiated) {} 2. What has DispatchQueue.main.async { } been replaced with?Worms
I will update the question to make it more clear what I am asking about.Worms
Please see this detailed articleChirpy
Thank you vadian! I have read this article, but didn't found the answer. I will keep searching the answer to my first question, if anyone can add something, that could help me, I would appreciate. Oh, and am I right by thinking I can replace DispatchQueue.global(qos: .userInitiated).async { } with Task(priority: .userInitiated) {}?Worms
This article might be better. It explains async/await in conjunction with URLSession and SwiftUI.Slippery
F
33

If you really are going to do something slow and synchronous, Task.detached is a closer analog to GCD’s dispatching to a global queue. If you just use Task(priority: ...) { ... } you are leaving it to the discretion of the concurrency system to decide which thread to run it on. (And just because you specify a lower priority does not guarantee that it might not run on the main thread.)

For example:

func fetchAndUpdateUI(from url: URL) {
    Task.detached {                          // or specify a priority with `Task.detached(priority: .background)`
        let data = try Data(contentsOf: url)
        let image = UIImage(data: data)
        await self.updateUI(with: image)
    }
}

And if you want to do the UI update on the main thread, rather than dispatching it back to the main queue, you would simply add the @MainActor modifier to the method that updates the UI:

@MainActor
func updateUI(with image: UIImage?) async {
    imageView.image = image
}

That having been said, this is a pretty unusual pattern (doing the network request synchronously and creating a detached task to make sure you don't block the main thread). We would probably use URLSession’s new asynchronous data(from:delegate:) method to perform the request asynchronously. It offers better error handling, greater configurability, participates in structured concurrency, and is cancelable.

In short, rather than looking for one-to-one analogs for the old GCD patterns, use the concurrent API that Apple has provided where possible.


FWIW, in addition to the @MainActor pattern shown above (as a replacement for dispatching to the main queue), you can also do:

await MainActor.run {
    …
}

That is roughly analogous to the dispatching to the main queue. In WWDC 2021 video Swift concurrency: Update a sample app, they say:

In Swift’s concurrency model, there is a global actor called the main actor that coordinates all operations on the main thread. We can replace our DispatchQueue.main.async with a call to MainActor’s run function. This takes a block of code to run on the MainActor. …

But he goes on to say:

I can annotate functions with @MainActor. And that will require that the caller switch to the main actor before this function is run. … Now that we've put this function on the main actor, we don’t, strictly speaking, need this MainActor.run anymore.

Franky answered 22/11, 2021 at 18:19 Comment(2)
What if you want to do something in the background thread and then finish up with result in main, which would have nothing todo with URLSession? user14119170 question is really reasonable, because DispatchQueue was indeed used for such cases as well. Therefore, what's the right way to do a background thread with async/await concurrency outside URLSession examples?Scowl
As I outlined above, a detached task is the simplest way to run some computationally intensive task on background thread. (You can also do it with actors.) My point re URLSession was merely that the OP’s example of synchronous network request is a poor use-case.Franky

© 2022 - 2024 — McMap. All rights reserved.