I always thought that using Task
would automatically run time-consuming tasks on a background thread, keeping the UI responsive.
However, in the following code, I noticed that my fetchNotes
function (which is time-consuming) still runs on the main UI thread because UIViewController
is marked as @MainActor
.
Is this something to be concerned about? Will it affect the UI’s responsiveness if a time-consuming task runs on the main UI thread?
Interestingly, during my tests, even though fetchNotes
takes a few seconds to complete, my UI doesn’t freeze. Why isn’t the UI freezing when the main UI thread is handling a time-consuming operation?
Should I ever consider using Task.detached
?
Here’s my code snippet.
class MainViewController: UIViewController {
private func fetchNotesAsync() {
print(">>>> fetchNotesAsync (Thread.isMainThread)") // true
Task {
print(">>>> Task (Thread.isMainThread)") // true
let noteWrappers = await fetchNotes()
...
}
}
private func fetchNotes() async -> [NoteWrapper] {
// Executing firebase query.getDocuments() to retrieve remote documents.
// query.getDocuments() is a firebase library async function
let querySnapshot = try await query.getDocuments()
}
2
Answers
The non-async code in the
Task { ... }
andfetchNotes
are indeed running on the main actor, butquery.getDocuments
is not, assuming it isQuery.getDocuments
.getDocuments
is not declared to be isolated to any actor, and it is also not declared in an actor-isolated type. Therefore,getDocuments
is nonisolated. Nonisolated async methods are always run on some thread in the cooperative thread pool.If you use
Task.detached
, the synchronous code in theTask
will be run on a thread in the cooperative thread pool, but not the synchronous code infetchNotes
becausefetchNotes
is declared in a class isolated to the main actor. You need to additionally declarefetchNotes
asnonisolated
for the synchronous code in its body to run off of the main actor.tl;dr
Your code snippet is fine, and the main actor will not be blocked.
You said:
Let us first outline what is going on:
The
fetchNotesAsync
is isolated to the main actor, because it is a method of a class happens to be isolated to the main actor.The
Task
infetchNotesAsync
is isolated to the main actor, too, becauseTask {…}
creates a top-level task on behalf of the current actor (i.e.,fetchNotesAsync
’s actor).The
fetchNotes
also is isolated to the main actor, because it is a method of a class happens to be isolated to the main actor.Note: The fact that this was called from
Task {…}
infetchNotesAsync
(which also happens to be isolated to the main actor) is completely irrelevant. ThefetchNotes
is isolated to the main actor solely by the fact that it was an isolated function of actor-isolated type, aUIViewController
subclass. Actor isolation is dictated by how the function was defined, not by who called it.So, no,
Task
does not “run time-consuming tasks on a background thread”. (In fact, again,Task {…}
will actually create a new top-level task on behalf of the current actor.) The reason thatfetchNotes
does not block the main actor is simply because it performs anawait
of anasync
function,getDocuments
. So, whilegetDocuments
runs, the main actor is free to do other things, namely to keep the UI responsive.You noted:
Yep, that is correct.
But an aside regarding your code snippet: It does not really check to see
fetchNotes
runs on the main thread. You are checking thatfetchNotesAsync
runs on the main thread (which makes sense because that is isolated to the main actor). You are also checking that itsTask {…}
runs on the main thread (which makes sense, becausefetchNotesAsync
is, too). But neither of those has any bearing on which threadfetchNotes
runs. As you said, that is dictated solely by the fact that it is an isolated function of a class isolated to the main actor.If you really wanted to check to see if
fetchNotes
was on the main actor, you need to do that insidefetchNotes
, itself:So, two observations:
You have to check inside
fetchNotes
, not from where you called it.You should not use
Thread
API from Swift concurrency. The compiler is allowed to do all sorts of fancy optimizations that may makeThread
-based checks invalid. In fact, in Swift 6 will not even permit this test within an asynchronous context. UseassertIsolated
to test whether you are on the main actor or not.Now, setting these two points aside, it turns out that
fetchNotes
really is isolated to the main actor. But, the fact thatgetDocuments
might take a little time is not a concern, but because youawait
it; the main actor is free to go on to do other things whilefetchNotes
is suspended, awaiting the results ofgetDocuments
.And the context used by
getDocuments
has nothing to do with the actor isolation offetchNotes
. Unlike GCD, where the caller’s context often dictated what queue a called function used, in Swift concurrency, the question is howgetDocuments
was implemented, and the actor isolation offetchNotes
is immaterial.But, bottom line, you are correct that
fetchNotes
is isolated to the main actor, because it is isolated member of thisUIViewController
subclass. ButgetDocuments
will not block the main actor.Nope. This is the beauty of Swift concurrency: If you merely
await
otherasync
functions, then the caller’s concurrency context will not be blocked.You continued:
Yep, the main actor is not blocked, and therefore the UI will not freeze. Yes,
fetchNotes
will suspend untilgetDocuments
returns a value, but the main actor, itself, is not blocked.And you said:
Because you
await query.getDocuments()
function, the main actor is not blocked.This is not a use-case for a detached task.