Why doesn’t this MainActor lock the UI

I’m a real concurrency novice but used it to continuously call a function that takes about 1/4 second to run. Initially I used “Task.detached {“ thinking this is a hard loop, there’s no awaits, so it needs to be in the background so as to not lock up the UI.

Then as an experiment I tried “Task { @MainActor in” which I thought should lock up the UI but didn’t. The cancel button still functions to stop the task.

I thought “@MainActor in” meant the code in the closure was running on the main actor which is where the UI is running, so if this loop is running and never yielding then how is the UI still working? Or is my code yielding somehow without any awaits?

Or could this be an anomaly of my environment: iPad Playgrounds (Swift 5.10)?

struct MyView: View {
    @State var theTask: Task<Void, Never>?
    var body: some View {
        VStack {
            
            Button("run sim") {
                guard theTask == nil else { return }
                theTask = Task { @MainActor in  // <<<<<<< shouldn’t this MainActor lock the UI?
                    var aboveHalf = 0
                    for i in 1...9999999 {
                        let v = runSimulation()
                        if v >= 0.5 { aboveHalf += 1 }
                        print("\(i), \(aboveHalf)")
                        if Task.isCancelled { break }
                    }
                }
            }
            
            Button("stop sim") {  // this button still works
                theTask?.cancel()
                theTask = nil
            }
            
        }
    }
}

func runSimulation() -> Double { ... }  // takes about 1/4 second
1 Like

Based on your result, I guess it’s Task.isCancelled suspends the current func so that the cancel button’s handler (and other funcs) can run in main thread. It actually makes sense, because otherwise other code have no chance to set the cancel flag and the Task.isCancelled check becomes meaningless.

EDIT: There is one way to prove it. You can modify the simulation func to take 40 seconds. I believe cancel button can only respond after about 40 seconds.

1 Like

Tbh I'd probably challenge your observation? It'd be useful to have more exact output information to confirm what you're seeing.

The action of the Button is @MainActor (init(_:action:) | Apple Developer Documentation), and your task is indeed @MainActor those two cannot execute in parallel.

That's not the case; and you can know this by observing that there's no await on that line -- it is not possible to suspend unless there's an await on a line. So even without knowing impl details, you can be certain there's no suspension happening there.

2 Likes

OK, I went back and did more experiments and for a time it seemed print() was the culprit because taking it out did finally lock up the UI. But then I kept experimenting and preparing a complete post of code and now it won’t reproduce anymore. The UI locks up even with print() back in place so idk. I guess I confused myself or something. Sorry for the noise, I’ll post again if I discover what was going on. Thanks rayx and ktoso for your ideas :+1:

Oh also, while researching this I talked with an AI and it was confirming that an ‘await’ is the only place a suspension point can occur but on further perusing of docs it seems Task.yield and Task.sleep are 2 exceptions to this. Asking the AI about this it goes “oh yeah those can switch the process too” lol. So as far as I know await, .yield, and .sleep and the only places a task will or might suspend.

Both Task.yield() and Task.sleep() also require await (because they are async functions), so they are not special. Every potential suspension point is always marked with an await.

4 Likes

Oh perfect, thank you, I didn’t know that

Did you try it in a real Xcode project?

I used this.

func runSimulation() -> Double {
    let u = ContinuousClock ()
    let t = u.now + .seconds (0.25)
    while true {
        if u.now >= t {
            break
        }
    }
    return 0.7
}

1 Like

I have such a hard time with Xcode. I thought about trying but it’s always a massive hassle and didn’t want to further complicate things. In the end I’m pretty sure I just messed up and was testing the wrong view and iPad Playgrounds was working correctly the whole time.

And thanks for the demo of ContinuousClock, that’ll be helpful in my continuing (and more careful:) explorations of concurrency. I’ve been reticent to wade into this area but it’s slowly making sense as I try more.