That's an interesting point. I wonder if the compiler will simply inline very short tasks. My guess is that it won't, and that scheduling is always done dynamically if you manually call Task
(even if it's executed serially after that). On the other hand, if the task is something like a one-liner variable change, the compiler should have enough information to know that it's better to just inline it. Hmm.
This is what I get for testing one thing and assuming it is the other. So it looks like global actors are more aggressive than non-global actors, and so all child tasks of a global actor will also require that global actor implicitly, unless you detach the task. Yet my tests show that non-global actors will pick up on isolation based on if isolated properties are accessed or not. This kinda makes sense… a global actor doesn’t have any particular state to isolate, so it’s the task itself that is isolated.
I’ve attached some code you can run in a playground that demonstrates what I mean. For both the global actor and main actor, the child task cannot run until the current task suspends, which is when Task.sleep() is called. Since this is a true suspension point, you will see doAThing() continue to execute until it reaches that suspension point, which is where the child task gets a chance to run. For the non-global actor, the child task runs in parallel, and multiple runs will show different ordering of the print statements. But since you can see that in many cases, the ordering would make no sense if the code was running within the actor. But the moment you uncomment the “self.isActive = true” within the child task, it behaves the same as the global and main actors.
As for checking threads, I don’t do a ton of that unless there’s a specific issue I’m trying to track down, as Swift’s scheduler is inherently lazy. You’ll tend to stay on a thread until a suspension point is reached, but when you awake from suspend what thread you awaken on will depend on a few factors, such as if the function itself is isolated or not, and if it is isolated, is it a global actor or not. So from my experience it’s generally better to consider what actor you are on.
But as you‘ve shown me here, the implicit nature of child tasks taking on global actor isolation of the parent, and the fact that many SwiftUI and UIKit types are annotated with @MainActor means you can find yourself isolated on the MainActor without knowing it. That’s not a super-great for someone new to this, even if it makes some level of sense after thinking through it.
So ultimately, if you want to run something that needs to detach from the global actor it is a part of and run in a non-isolated context (and UI code will generally be isolated to the main actor), then you must use Task.detached manually. But this generally should be the case where you might do a lot of work before a suspend point can pass the global actor off to other work.
EDIT: Just want to add that I have really appreciated the back and forth here. It’s really helped me solidify my understanding around Swift concurrency and how to use it better.
Code:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
actor MyActor {
private var isActive: Bool = false
func doAThing() async {
guard !isActive else { return }
isActive = true
let task = Task { () -> Int in
print("Starting Sub Task")
// Uncomment the below to force actor isolation on this.
//self.isActive = true
return await SomeStruct.doSomething(2)
}
print("Herf")
let someResult = await SomeStruct.doSomething(1)
print("Derf")
print(someResult)
isActive = false
print("Waiting...")
print(await task.result)
}
}
struct SomeStruct {
static func doSomething(_ value: Int) async -> Int {
print("hello \(value)")
try! await Task.sleep(nanoseconds: 10000)
return value
}
}
@MainActor class MyMainActorClass {
private var isActive: Bool = false
func doAThing() async {
guard !isActive else { return }
isActive = true
let task = Task { () -> Int in
print("Starting Sub Task")
//self.isActive = true
return await SomeStruct.doSomething(2)
}
print("Herf")
let someResult = await SomeStruct.doSomething(1)
print("Derf")
print(someResult)
isActive = false
print("Waiting...")
print(await task.result)
}
}
@globalActor
struct MyGlobalActor {
actor ActorType { }
static let shared: ActorType = ActorType()
}
@MyGlobalActor class MyGlobalActorClass {
private var isActive: Bool = false
func doAThing() async {
guard !isActive else { return }
isActive = true
let task = Task { () -> Int in
print("Starting Sub Task")
//self.isActive = true
return await SomeStruct.doSomething(2)
}
print("Herf")
let someResult = await SomeStruct.doSomething(1)
print("Derf")
print(someResult)
isActive = false
print("Waiting...")
print(await task.result)
}
}
enum RunMode {
case actor
case mainActor
case globalActor
}
let runMode: RunMode = .actor
if runMode == .actor {
let actor = MyActor()
Task { @MainActor in
await actor.doAThing()
print("Actor Task complete")
PlaygroundPage.current.finishExecution()
}
print("Actor Task created")
} else if runMode == .globalActor {
Task { @MyGlobalActor in
let actorClass = MyGlobalActorClass()
await actorClass.doAThing()
print("GlobalActor Task Complete")
PlaygroundPage.current.finishExecution()
}
print("GlobalActor Task Created")
} else {
Task { @MainActor in
let mainActorClass = MyMainActorClass()
await mainActorClass.doAThing()
print("MainActor Task Complete")
PlaygroundPage.current.finishExecution()
}
print("MainActor Task Created")
}