[Warning! In case anyone runs across this question: it was asked very early in the history of the introduction of async/await into Swift. The problem it poses was probably a bug, indeed; but that bug is gone. The behavior described in the question is no longer reproducible. I've added a note at the end of the question, saying what happens now.]
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.
Historical conclusion: In Swift 6, where Swift Concurrency and async/await
have been fully rationalized, the problem no longer arises. func test()
is @MainActor
by default, because UIViewController is @MainActor
, and all lines of it execute on the main actor. If func test()
is declared nonisolated
, then all lines of it execute off the main actor.
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.