Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

asyncDetached falling back into main thread after MainActor call

I'm trying out the new async/await stuff. My goal here is to run the test() method in the background, so I use Task.detached; but during test() I need to make a call on the main thread, so I'm using MainActor.

(I realize that this may look convoluted in isolation, but it's pared down from a much better real-world case.)

Okay, so test code looks like this (in a view controller):

override func viewDidLoad() {
    super.viewDidLoad()
    Task.detached(priority: .userInitiated) {
        await self.test()
    }
}
@MainActor func getBounds() async -> CGRect {
    let bounds = self.view.bounds
    return bounds
}
func test() async {
    print("test 1", Thread.isMainThread) // false
    let bounds = await self.getBounds()
    print("test 2", Thread.isMainThread) // true
}

The first print says I'm not on the main thread. That's what I expect.

But the second print says I am on the main thread. That isn't what I expect.

It feels as if I've mysteriously fallen back into the main thread just because I called a MainActor function. I thought I would be waiting for the main thread and then resuming in the background thread I was already on.

Is this a bug, or are my expectations mistaken? If the latter, how do I step out to the main thread during await but then come back to the thread I was on? I thought this was exactly what async/await would make easy...?

(I can "solve" the problem, in a way, by calling Task.detached again after the call to getBounds; but at that point my code looks so much like nested GCD that I have to wonder why I'm using async/await at all.)

Maybe I'm being premature but I went ahead and filed this as a bug: https://bugs.swift.org/browse/SR-14756.


More notes:

I can solve the problem by replacing

    let bounds = await self.getBounds()

with

    async let bounds = self.getBounds()
    let thebounds = await bounds

But that seems unnecessarily elaborate, and doesn't convince me that the original phenomenon is not a bug.


I can also solve the problem by using actors, and this is starting to look like the best approach. But again, that doesn't persuade me that the phenomenon I'm noting here is not a bug.


I'm more and more convinced that this is a bug. I just encountered (and reported) the following:

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    async {
        print("howdy")
        await doSomeNetworking()
    }
}
func doSomeNetworking() async {
    print(Thread.isMainThread)
}

This prints howdy and then the second print prints true. But if we comment out the first print, the remaining (second) print prints false!

How can merely adding or removing a print statement change what thread we're on? Surely that's not intended.

like image 961
matt Avatar asked Jun 10 '21 02:06

matt


2 Answers

As I understand it, given that this is all very new, there is no guarantee that asyncDetached must schedule off the main thread.

In the Swift Concurrency: Behind the Scenes session, it's discussed that the scheduler will try to keep things on the same thread to avoid context switches. Given that though, I don't know how you would specifically avoid the main thread, but maybe we're not supposed to care as long as the task makes progress and never blocks.

I found the timestamp (23:18) that explains that there is no guarantee that the same thread will pick up a continuation after an await. https://developer.apple.com/videos/play/wwdc2021/10254/?time=1398

like image 183
fullsailor Avatar answered Sep 27 '22 17:09

fullsailor


The following formulation works, and solves the entire problem very elegantly, though I'm a little reluctant to post it because I don't really understand how it works:

override func viewDidLoad() {
    super.viewDidLoad()
    Task {
        await self.test2()
    }
}
nonisolated func test2() async {
    print("test 1", Thread.isMainThread) // false
    let bounds = await self.view.bounds // access on main thread!
    print("test 2", bounds, Thread.isMainThread) // false
}

I've tested the await self.view.bounds call up the wazoo, and both the view access and the bounds access are on the main thread. The nonisolated designation here is essential to ensuring this. The need for this and the concomitant need for await are very surprising to me, but it all seems to have to do with the nature of actors and the fact that a UIViewController is a MainActor.

like image 30
matt Avatar answered Sep 27 '22 16:09

matt