`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:
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
}
}
}
}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:
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 inMain
thread, but not. Should I useDispatchQueue.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
Swift - Reorder Uitableview Cells
Waiting for Alamofire in Unit Tests
Swift Realm Property '*' Has Been Added to Latest Object Model Migration
Swift: Programmatically Enumerate Outgoing Segues from a Uiviewcontroller
Swift [1,2] Conforms to Anyobject But [Enum.A, Enum.B] Does Not
How to Use a Variadic Closure in Swift
What's the Best Way to Iterate Over Results from an API, and Know When It's Finished
How to Make Swiftui Listmenu with Different Behaviors
How to Convert Anyclass to a Specific Class and Init It Dynamically in Swift
iOS 13: Threading Violation: Expected the Main Thread
Swift & Firebase - How to Store More User Data Other Than Email and Password
Xcode 11 Beta 3, Build Error "Unknown Attribute 'State'", "Use of Undeclared Type 'View'" etc
How to Prevent Timer Slowing Down in Background
Load Desktop Version Wkwebview iOS 9
Binding in a Foreach in Swiftui
What Are the Advantages Swift Deprecates C-Style for Statement