I try to get familiar with Swift concurrency, especially MainActor.
I made myself this demo-class:
@MainActor
class ThreadsDemo {
let range1000 = 0.. < 1000
var randomNumber = 0
init() {
randomNumber = Int.random(in: range1000)
}
func modifyRandomNumber() async {
print("2. isMain: \(Thread.isMainThread)")
let newRandomNumber = Int.random(in: range1000)
print("Generated random-number -> \(newRandomNumber)")
Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) {
_ in
print("4. isMain (within wait): \(Thread.isMainThread)")
}
print("5. isMain: \(Thread.isMainThread)")
randomNumber = newRandomNumber
}
}
Invoking the ThreadDemo-method:
.task {
print("1. isMain: \(Thread.isMainThread)")
print("Initial random-number -> \(threadsDemo.randomNumber)")
await threadsDemo.modifyRandomNumber()
print("Modified random-number -> \(threadsDemo.randomNumber)")
print("6. isMain: \(Thread.isMainThread)")
}
Result:
1. isMain: true
Initial random-number -> 455
2. isMain: true
Generated random-number -> 578
5. isMain: true
Modified random-number -> 578
6. isMain: true
4. isMain (within wait): true
Obviously runs everything on the main-thread, even the block within the scheduled timer.
I'm confused, because I thought it would use background-threads to prevent the UI from freezing and becoming unresponsive.
If it runs everything on the main-thread anyway, then what's the purpose of using tasks?
But what I actually tried to find out:
Does the @MainActor-annotation result in running everything on the main-thread (all executable code) or does it only result in making changes to the state (assignments, etc.), always on the main-thread?
If it runs everything on the main-thread anyway, then what's the purpose of using tasks?
A Task
or .task { ... }
allows you to run code asynchronously. It allows you to call async
methods, and await
things. It doesn't have to be on another thread. In this case, all the methods that you are calling during the .task { ... }
all happen to be main actor-isolated, so they all run on the main thread.
If you happen to call a non-isolated async
method, or a method isolated to a different actor, then the code in that method will be run on a different thread.
struct ContentView: View {
var body: some View {
Color.clear
.task {
await foo()
}
}
}
// global functions are implicitly nonisolated
func foo() async {
// you should use assertIsolated instead of
// Thread.isMainThread in async contexts
MainActor.assertIsolated() // this will crash
}
Does the @MainActor-annotation result in running everything on the main-thread (all executable code) or does it only result in making changes to the state (assignments, etc.), always on the main-thread?
Arguably, all code changes some state (the program counter, at least), so the two options you stated aren't really distinguishable.
A main actor-isolated method makes all the synchronous parts of its body run on the main thread, i.e. the parts where you don't await
. Once you await
something, which thread the code is run depends on that "something". If it also happens to be isolated to the main actor, then it will be run on the main thread. Otherwise it won't be on the main thread. This is not just true for the main actor, but for any actor isolation in general.
even the block within the scheduled timer.
This part is not related to Swift concurrency at all. Timer.scheduledTimer
runs a timer on the current run loop. Since this is called on the main thread, the timer will be run on the run loop associated with the main thread, which obviously will be run on the main thread.