Create an account

Very important

  • To access the important data of the forums, you must be active in each forum and especially in the leaks and database leaks section, send data and after sending the data and activity, data and important content will be opened and visible for you.
  • You will only see chat messages from people who are at or below your level.
  • More than 500,000 database leaks and millions of account leaks are waiting for you, so access and view with more activity.
  • Many important data are inactive and inaccessible for you, so open them with activity. (This will be done automatically)


Thread Rating:
  • 518 Vote(s) - 3.57 Average
  • 1
  • 2
  • 3
  • 4
  • 5
`Task` blocks main thread when calling async function inside

#1
I have an `ObservableObject` class and a SwiftUI view. When a button is tapped, I create a [`Task`](

[To see links please register here]

) and call `populate` (an async function) from within it. I thought this would execute `populate` on a background thread but instead the entire UI freezes. Here's my code:


```
class ViewModel: ObservableObject {
@Published var items = [String]()
func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}
self.items = items
}
}

struct ContentView: View {
@StateObject var model = ViewModel()
@State var rotation = CGFloat(0)

var body: some View {
Button {
Task {
await model.populate()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(model.items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}
```

Result:

<img src="https://i.stack.imgur.com/TnDMi.gif" width="200" alt="Rotation animation freezes when the button is pressed">

The button stops moving, then suddenly snaps back when `populate` finishes.

Weirdly, if I move the `Task` into `populate` itself and get rid of the `async`, the rotation animation doesn't stutter so I think the loop actually got executed in the background. However I now get a `Publishing changes from background threads is not allowed` warning.

```
func populate() {
Task {
var items = [String]()
for i in 0 ..< 4_000_000 {
items.append("\(i)")
}
self.items = items /// Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
}
}

/// ...

Button {
model.populate()
}
```

Result:

<img src="https://i.stack.imgur.com/kqkuK.gif" width="200" alt="Rotation animation continues even when the button is pressed">

How can I ensure my code gets executed on a background thread? I think this might have something to do with `MainActor` but I'm not sure.
Reply

#2
**Update 6/10/22:** At WWDC I asked some Apple engineers about this problem — it really is all about actor inheritance. However, there were some compiler-level changes in Xcode 14 Beta. For example, this will run smoothly on Xcode 14, but lag on Xcode 13:


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

func populate() async {
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}

/// explicitly capture `items` to avoid `Reference to captured var 'items' in concurrently-executing code; this is an error in Swift 6`
Task { @MainActor [items] in
self.items = items
}
}
}

struct ContentView: View {
@StateObject var model = ViewModel()
@State var rotation = CGFloat(0)

var body: some View {
Button {
Task {

/// *Note!* Executes on a background thread in Xcode 14.
await self.model.populate()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(model.items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}
```

Again, `Task` inherits the context of where it's called.
- A `Task` called from within a plain `ObservableObject` class will run in the background, since the class isn't a main actor.
- A `Task` called inside a `Button` will probably run on the main actor, since the `Button` is a UI element. *Except, Xcode 14 changed some things and it actually runs in the background too...*

To make sure a function runs on the background thread, independent of the inherited actor context, you can add `nonisolated`.

```
nonisolated func populate() async {

}
```


Note: the [Visualize and optimize Swift concurrency](

[To see links please register here]

) video is super helpful.
Reply

#3
As others have mentioned, the reason of this behavior is that the `Task.init` inherits the actor context automatically. You're calling your function from the button callback:

Button {
Task {
await model.populate()
}
} label: {

}
The button callback is on the main actor, so the closure passed to the `Task` initializer is on the main actor too.

One solution is using a detached task:

func populate() async {
Task.detached {
// Calculation here
}
}

While detached tasks are unstructured, I'd like to suggest structured tasks like `async let` tasks:

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

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

items = await newItems
}
}

This is useful when you want the `populate` function to return some value asynchronously. This structured task approach also means cancellation can be propagated automatically. For example, if you want to cancel the calculation when the button is tapped multiple times in a short time, you can do something like this:

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

func populate() async {
async let newItems = { () -> [String] in
var items = [String]()
for i in 0 ..< 4_000_000 {
// Stop in the middle if cancelled
if i % 1000 == 0 && Task.isCancelled {
break
}
items.append("\(i)")
}
return items
}()

items = await newItems
}
}

struct ContentView: View {
@StateObject var model: ViewModel
@State var task: Task<Void, Never>?

init() {
_model = StateObject(wrappedValue: ViewModel())
}

var body: some View {
Button {
task?.cancel() // Cancel previous task if any
task = Task {
await model.populate()
}
} label: {
// ...
}
}
}

Moreover, `withTaskGroup` also creates structured tasks and you can avoid inheriting the actor context too. It can be useful when your computation has multiple child tasks that can progress concurrently.
Reply

#4
Consider:

```swift
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:

```swift
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:

```swift
@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:

```swift
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:

[![enter image description here][1]][1]

- - -

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


[1]:

[2]:

[To see links please register here]

[3]:

[To see links please register here]

[4]:

[To see links please register here]

Reply

#5
First, you can't have it both ways; Either you perform your CPU intensive work on the main thread (and have a negative impact on the UI) or you perform the work on another thread, but you need to explicitly dispatch the UI update onto the main thread.

However, what you are really asking about is

> (By using `Task`) I thought this would execute populate on a background thread but instead the entire UI freezes.

When you use a `Task` you are using [*unstructured concurrency*](

[To see links please register here]

), and when you initialise your `Task` via [init(priority:operation)](

[To see links please register here]

) *the task ... inherits the priority and actor context of the caller*.

While the `Task` is executed asynchronously, it does so using the actor context of the caller, which in the context of a `View` `body` is the main actor. This means that while your task is executed asynchronously, it still runs on the main thread and that thread is not available for UI updates while it is processing. So you are correct, this has everything to do with `MainActor`.

When you move the `Task` into `populate` it is no longer being created in a `MainActor` context and therefore does not execute on the main thread.

As you have discovered, you need to use this second approach to avoid the main thread. All you need to do to your code is move the final update back to the main queue using the `MainActor`:

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

You could also use `Task.detached()` in the body context to create a `Task` that is not attached the `MainActor` context.
Reply

#6
You can fix it by removing the class. You aren't using Combine so you don't need its `ObservableObject` and SwiftUI is most efficient if you stick to value types. The button doesn't hang with this design:

extension String {
static func makeItems() async -> [String]{
var items = [String]()
for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
items.append("\(i)")
}
return items
}
}

struct AnimateContentView: View {
@State var rotation = CGFloat(0)
@State var items = [String]()

var body: some View {
Button {
Task {
items = await String.makeItems()
}
} label: {
Color.blue
.frame(width: 300, height: 80)
.overlay(
Text("\(items.count)")
.foregroundColor(.white)
)
.rotationEffect(.degrees(rotation))
}
.onAppear { /// should be a continuous rotation effect
withAnimation(.easeInOut(duration: 2).repeatForever()) {
rotation = 90
}
}
}
}

Reply



Forum Jump:


Users browsing this thread:
1 Guest(s)

©0Day  2016 - 2023 | All Rights Reserved.  Made with    for the community. Connected through