Do you actually need to run a()
in a separate task from main()
or are you only trying to unblock the main actor?
If the latter, you can simply await a()
in main()
. Since a
is not isolated to the main actor, swift will automatically hop off the main actor so it wouldn't be free to execute UI work. Once a
returns, main
will be enqueued back on the main actor to continue execution there.
@MainActor func main() {
// on main actor
// a is async without a specific isolation so we will hop off the main actor here and to the global concurrent executor
await a()
// once a finishes we return to (enqueue on) the main actor here.
// the main actor is free to execute other code while a is running
}
(There is currently some discussion around changing how this works exactly in the future but you could still get the same effect with slightly adjusted syntax.)
If you do need the separate task to run a
in and want a structured subtask, then async let or TaskGroups are indeed the way to go. That wouldn't make much sense for the simplified example but if e.g. main
wanted to start several pieces of work that could be executed concurrently, then you could start them using async let or TaskGroups and wait for them to finish.
If you were going to use async let to start multiple async operations you should probably be able to get rid of the Constant 'a' inferred to have type '()', which may be unexpected
warning by explicitly specifying the type in this case:
async let a: () = a()
An async let that doesn't produce a value and is just used to await the end of an operation is probably somewhat rare and might be a mistake so the compiler just wants you to be explicit that this is actually intended and not a mistake.
Probably no longer relevant to your problem but I also added some discussion for how you could make a
synchronous and call it from a (main actor) isolated Task and make sure it runs nonisolated.
Sidebar
As to your original question how a
could be made synchronous while used from a Task
, the issue there is that Task {}
inherits the isolation of its context so in a @MainActor
function the top level task code is main actor isolated as well and so a synchronous method call ends up on the main actor.
As you noted, Task.detached {}
is one way to avoid that since Task.detached {}
doesn't inherit the isolation context.
Generally, just keeping a
async is probably the best solution since it currently (this could change in the future) ensures it will run on the global concurrent executor. Even if you don't do any other async work in the function, a
being async still has value in that it allows us to switch executors when calling it. Only async calls can change executors, so we need an await somewhere to change the executor.
But if a
really needs to be synchronous for some other reason, you could ensure it isn't run on the main actor as follows:
@MainActor
func onMainActor() {
// option 1), create the task in a nonisolated function -> hop off the main actor here
await spawnATask()
// option 2), keep the task main actor isolated but call the synchronous function indirectly via a nonisolated async function
// This is slightly worse than option 1) if we just want to call a since we need to start and end the task on the main actor unnecessarily but could be useful in some cases where we also need to do some main actor isolated work in the task anyway
Task {
// we inherit the main actor isolation here and start execution on the main actor
// call an async function that isn't main actor isolated -> we hop off the main actor
await asyncA()
// and return to the main actor here...
}
}
func spawnATask() async {
// async function without specific isolation -> running on the global concurrent executor
Task {
// the task inherits the isolation -> also running on the global concurrent executor
a()
}
}
func asyncA() async {
// async function without specific isolation -> we are running on the global concurrent executor
a()
}
func a() {}
If Closure Isolation Control is proposed and accepted in the future, this would get a lot simpler:
@MainActor
func onMainActor() {
// Use nonisolated to ensure the task doesn't inherit isolation and is instead directly enqueued on the global concurrent executor.
Task { nonisolated in
// starts execution on the global concurrent executor
a()
}
}