Task in @MainActor Function Appears to Become Detached

I'm hoping someone can help me understand some unexpected behavior in a @MainActor annotated Function which calls a task that calls a method on a background actor.

With non-annotated normal functions, the function would call the task, pause until the task completes, and finish the end of the function.

However, when the function is annotated @MainActor, the internal task appears to become detached and execute asynchronously such that it finishes after the end of the @MainActor annotated function.

The code below demonstrates this behavior in a playground:

actor SeparateActor{
	func actorFunc(_ str:String){
		print("\tActorFunc(\(str))")
	}
}

class MyClass{
	var sa = SeparateActor()
	
	func normalFuncWithTask(){
		print("normalFuncWithTask Start")
		Task{
			await self.sa.actorFunc("normalFuncWithTask")
		}
		print("normalFuncWithTask End")
	}

	@MainActor func mainActorFunctionWithTask(){
		print("mainActorFunctionWithTask Start")
		Task{
			await self.sa.actorFunc("mainActorFunctionWithTask")
		}
		print("mainActorFunctionWithTask End")
	}
}


Task{
	let mc = MyClass()

	print("\nCalling normalFuncWithTask")
	mc.normalFuncWithTask()

	print("\nCalling mainActorFunctionWithTask")
	await mc.mainActorFunctionWithTask()	
}

I would expect to see the two functions above behave the same, however the "mainActorFunctionWithTask end" appears before the actor call is invoked.

Calling normalFuncWithTask
normalFuncWithTask Start
	ActorFunc(normalFuncWithTask)
normalFuncWithTask End

Calling mainActorFunctionWithTask
mainActorFunctionWithTask Start
mainActorFunctionWithTask End
	ActorFunc(mainActorFunctionWithTask)

Could anyone explain why the task appears to become detached when the function is annotated with @MainActor, and how to keep the internal task from becoming detached.

Many, many thanks!

I think you've piled on quite a bit of stuff here, so I'm not sure precisely what answer you're looking for. Let me try the easiest interpretation first.

Are you asking, in code like this:

Task {
    print("AAA") // or something async
}
print("BBB")

whether there's a predictable order of execution that would determine which print statement executes first?

Answer: no.

Note that, from this perspective, it wouldn't matter whether print("AAA") was a synchronous statement inside the task, or whether it was itself inside an async function call inside the task. There's no implicit ordering between a task (detached or not) and the line of code that immediately follows its creation.

Edit: Nor does it matter what actors are involved. The order of execution is unspecified.

That is wrong assumption initially. Both of your examples has undetermined execution order - if you wrap last task in a for loop like that:

for _ in 0..<100 {
    await Task { /* ... */ }.value
}

You will see that output differs for both functions over runs.

Is it possible to remove the Task in the method since it’s marked @MainActor it should be capable of being used in a sync context as an async method:


@MainActor 
func mainActorFunctionWithoutTask() async {
		print("mainActorFunctionWithTask Start")
		await self.sa.actorFunc("mainActorFunctionWithTask")
		print("mainActorFunctionWithTask End")
	}

Otherwise the task and end of function would basically race I think

Also, I think that in the first function which is not marked @MainActor there isn’t a guarantee that the suspended task will resume and complete its work before the end of the function.

My understanding has been that adding work to occur inside of a Task doesn’t come with any promises or gaurantees about when the work starts or finishes just that work will happen inside of a Task. Furthermore I think that any Task could get scheduled at any point on any thread at any moment and that may or may not include performing work from start to finish in the same time slice, or any time slice for that matter.

I’m not even sure if spawning a Task comes with any guarantees that the next line executes before the Task is scheduled to do anything at any time. As far as I can tell, this is corroborated by the docs. The docs describe this uncontrollable scheduling, where the author(s) state,

“When you create an instance of Task , you provide a closure that contains the work for that task to perform. Tasks can start running immediately after creation; you don’t explicitly start or schedule them…”

Here we can see that the docs suggest that they aren’t explicitly started or scheduled. This means that there isn’t any innate or implicit guarantee about when and for how long they perform any work.

Time slice here if interested https://angom.myweb.cs.uwindsor.ca/teaching/cs330/ch6.pdf page 22 of the silberschatz pdf

You're right that there's no such guarantee today.

However, we are working on providing more guarantees there; see this ongoing pitch: `@isolated(any)` function types