How to Avoid That My Swift Async Method Runs on the Main Thread in Swiftui

`Task` blocks main thread when calling async function inside

Consider:

func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}

You have marked populate with async, but there is nothing asynchronous here, and it will block the calling thread.

Then consider:

func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items
}
}

That looks like it must be asynchronous, since it is launching a Task. But this Task runs on the same actor, and because the task is slow and synchronous, it will block the current executor.

If you do not want it to run on the main actor, you can either:

  1. You can leave the view model on the main actor, but manually move the slow synchronous process to a detached task:

    @MainActor
    class ViewModel: ObservableObject {
    @Published var items = [String]()

    func populate() {
    Task.detached {
    var items = [String]()
    for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
    items.append("\(i)")
    }
    await MainActor.run { [items] in
    self.items = items
    }
    }
    }
    }
  2. For the sake of completeness, you could also make the view model to be its own actor, and only designate the relevant observed properties as being on the main actor:

    actor ViewModel: ObservableObject {
    @MainActor
    @Published var items = [String]()

    func populate() {
    Task {
    var items = [String]()
    for i in 0 ..< .random(in: 4_000_000...5_000_000) { // made it random so I could see values change
    items.append("\(i)")
    }
    await MainActor.run { [items] in
    self.items = items
    }
    }
    }
    }

I would generally lean towards the former, but both approaches work.

Either way, it will allow the slow process to not block the main thread. Here I tapped on the button twice:

Sample Image


See WWDC 2021 videos Swift concurrency: Behind the scenes, Protect mutable state with Swift actors, and Swift concurrency: Update a sample app, all of which are useful when trying to grok the transition from GCD to Swift concurrency.

SwiftUI Concurrency: Run activity ONLY on background thread

While using Swift Concurrency system, you can use the Task.detached(...) constructor to spawn an unstructured detached task. You can additionally specify the .background task priority in the initializer arguments if you want to run it on background.

As the async function you are trying to run updates a property which triggers a view redraw (settings is declared as an ObservedObject and I assume data is a Published property), thus you must set this property from the main actor.

For this to work, you could do something like this:

struct ContentView: View {

@StateObject var settings = Settings()

var body: some View {
// Some view...
}
.task {
await loadData()
}

func loadData() async {
await Task.detached(priority: .background) {
// Call an api.
// Get some data using URLSession
await MainActor.run {
settings.data = data
}
}
}
}

What is the best solution for background task using Swift async, await, @MainActor

You asked:

I thought the class ViewModel is @MainActor, count property is manipulated in Main thread, but not. Should I use DispatchQueue.main.async {} to update count although @MainActor?

One should avoid using DispatchQueue at all. Just use the new concurrency system where possible. See WWDC 2021 video Swift concurrency: Update a sample app for guidance about transitioning from the old DispatchQueue code to the new concurrency system.

If you have legacy code with DispatchQueue.global, you are outside the new cooperative pool executor, and you cannot rely on an actor to resolve this. You would either have to manually dispatch the update back to the main queue, or, better, use the new concurrency system and retire GCD entirely.

When I tap “Start Task” button, button is pressed until the countupAsync() is done and not update “Count” on screen.

Yes, because it is running on the main actor and you are blocking the main thread with Thread.sleep(forTimeInterval:). This violates a key precept/presumption of the new concurrency system that forward progress should always be possible. See Swift concurrency: Behind the scenes, which says:

Recall that with Swift, the language allows us to uphold a runtime contract that threads will always be able to make forward progress. It is based on this contract that we have built a cooperative thread pool to be the default executor for Swift. As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.

Now that discussion is in the context of unsafe primitives, but it applies equally to avoiding blocking API (such as Thread.sleep(fortimeInterval:)).

So, instead, use Task.sleep(nanoseconds:), which, as the docs point out, “doesn’t block the underlying thread.” Thus:

func countUpAsync() async throws {
print("countUpAsync() isMain=\(Thread.isMainThread)")
for _ in 0..<5 {
count += 1
try await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
}
}

and

Button("Start Task") {
Task {
try await viewModel.countUpAsync()
}
}

The async-await implementation avoids blocking the UI.


In both cases, one should simply avoid old GCD and Thread API, which can violate assumptions that the new concurrency system might be making. Stick with the new concurrent API and be careful when trying to integrate with old, blocking API.


You said:

I want to run a long process and display the progress.

Above I told you how to avoid blocking with Thread.sleep API (by using the non-blocking Task rendition). But I suspect that you used sleep as a proxy for your “long process”.

Needless to say, you will obviously want to make your “long process” run asynchronously within the new concurrency system, too. The details of that implementation are going to be highly dependent upon precisely what this “long process” is doing. Is it cancelable? Does it call some other asynchronous API? Etc.

I would suggest that you take a stab at it, and if you cannot figure out how to make it asynchronous within the new concurrency system, post a separate question on that topic, with a MCVE.

But, one might infer from your example that you have some slow, synchronous calculation for which you periodically want to update your UI during the calculation. That seems like a candidate for an AsyncSequence. (See WWDC 2021 Meet AsyncSequence.)

func countSequence() async {
let stream = AsyncStream(Int.self) { continuation in
Task.detached {
for _ in 0 ..< 5 {
// do some slow and synchronous calculation here
continuation.yield(1)
}
continuation.finish()
}
}

for await value in stream {
count += value
}
}

Above I am using a detached task (because I have a slow, synchronous calculation), but use the AsyncSequence to get a stream of values asynchronously.

There are lots of different approaches (which are be highly dependent upon what your “long process” is), but hopefully this illustrates one possible pattern.



Related Topics



Leave a reply



Submit