What Determines Whether a Swift 5.5 Task Initializer Runs on the Main Thread

How can we know which Thread is used to execute Task?

UIViewController is marked @MainActor which means that tasks will be dispatched on the main thread. Your X class is not marked @MainActor so tasks are dispatched on any available thread.

asyncDetached falling back into main thread after MainActor call

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.

Can I use actors in Swift to always call a function on the main thread?

Actors in Swift 5.5 ‍♀️

Actor isolation and re-entrancy are now implemented in the Swift stdlib. So, Apple recommends using the model for concurrent logic with many new concurrency features to avoid data races. Instead of lock-based synchronisation (lots of boilerplate), we now have a much cleaner alternative.

Some UIKit classes, including UIViewController and UILabel, now have out of the box support for @MainActor. So we only need to use the annotation in custom UI-related classes. For example, in the code above, myImageView.image would automatically be dispatched on the main queue. However, the UIImage.init(named:) call is not automatically dispatched on the main thread outside of a view controller.

In the general case, @MainActor is useful for concurrent access to UI-related state, and is the easiest to do even though we can manually dispatch too. I've outlined potential solutions below:

Solution 1

The simplest possible. This attribute could be useful in UI-Related classes. Apple have made the process much cleaner using the @MainActor method annotation:

@MainActor func setImage(thumbnailName: String) {
myImageView.image = UIImage(image: thumbnailName)
}

This code is equivalent to wrapping in DispatchQueue.main.async, but the call site is now:

await setImage(thumbnailName: "thumbnail")

Solution 2

If you have Custom UI-related classes, we can consider applying @MainActor to the type itself. This ensures that all methods and properties are dispatched on the main DispatchQueue.

We can then manually opt out from the main thread using the nonisolated keyword for non-UI logic.

@MainActor class ListViewModel: ObservableObject {
func onButtonTap(...) { ... }

nonisolated func fetchLatestAndDisplay() async { ... }
}

We don't need to specify await explicitly when we call onButtonTap within an actor.

Solution 3 (Works for blocks, as well as functions)

We can also call functions on the main thread outside an actor with:

func onButtonTap(...) async {
await MainActor.run {
....
}
}

Inside a different actor:

func onButtonTap(...) {
await MainActor.run {
....
}
}

If we want to return from within a MainActor.run, simply specify that in the signature:

func onButtonTap(...) async -> Int {
let result = await MainActor.run { () -> Int in
return 3012
}
return result
}

This solution is slightly less cleaner than the above two solutions which are most suited for wrapping an entire function on the MainActor. However, actor.run also allows for inter threaded code between actors in one func (thx @Bill for the suggestion).

Solution 4 (Block solution that works within non-async functions)

An alternative way to schedule a block on the @MainActor to Solution 3:

func onButtonTap(...) {
Task { @MainActor in
....
}
}

The advantage here over Solution 3 is that the enclosing func doesn't need to be marked as async. Do note however that this dispatches the block later rather than immediately as in Solution 3.

Summary

Actors make Swift code safer, cleaner and easier to write. Don't overuse them, but dispatching UI code to the main thread is a great use case. Note that since the feature is still in beta, the framework may change/improve further in the future.

Since we can easily use the actor keyword interchangeably with class or struct, I want to advise limiting the keyword only to instances where concurrency is strictly needed. Using the keyword adds extra overhead to instance creation and so doesn't make sense when there is no shared state to manage.

If you don't need a shared state, then don't create it unnecessarily. struct instance creation is so lightweight that it's better to create a new instance most of the time. e.g. SwiftUI.

Swift - Not wait for async to return

You can wrap the function in a Task, Task

func doSomething() {
Task {
do {
try await a.doSomething()
} catch {
print(error)
}
}
}


Related Topics



Leave a reply



Submit