Task in @MainActor Function Appears to Become Detached

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

Normally, the function would call the task, pause until the task completes, and finish the end of the function.

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

The code below demonstrates this behavior in a playground:


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

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


Task{
	let mc = MyClass()

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

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

I would expect both the normalFunc and the mainActorFunc to behave the same, with the ActorFunc being called before the end of the task, but instead, my mainActor function completes before the task.

Calling normalFuncWithTask
normalFuncWithTask Start
	ActorFunc(normalFuncWithTask)
normalFuncWithTask End

Calling mainActorFunctionWithTask
mainActorFunctionWithTask Start
mainActorFunctionWithTask End
	ActorFunc(mainActorFunctionWithTask)

Replies

First up, I recommend that you avoid testing with top-level code. My experience is that it triggers all sorts of weirdness.

Second, if I compile your code with Strict Concurrency Checking set to Complete, the compiler complains about multiple problems. It’s best to fix such problems before trying to understand any weird behaviour you’re seeing.

Pasted in below is my version of your code, along with the errors I got. I’m building this with Xcode 15.3.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Dispatch

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

class MyClass{
	var sa = SeparateActor()
	
	@MainActor func mainActorFunctionWithTask(){
		print("mainActorFunctionWithTask Start")
		Task{
			await self.sa.actorFunc("mainActorFunctionWithTask")
		}
		print("mainActorFunctionWithTask End")
	}
	
	func normalFuncWithTask(){
		print("normalFuncWithTask Start")
		Task{
			await self.sa.actorFunc("normalFuncWithTask")
               // ^ Capture of 'self' with non-sendable type 'MyClass' in a `@Sendable` closure
		}
		print("normalFuncWithTask End")
	}
}

func main() {
    Task{
        let mc = MyClass()

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

        print("\nCalling mainActorFunctionWithTask")
        await mc.mainActorFunctionWithTask()
     // ^ Passing argument of non-sendable type 'MyClass' into main actor-isolated context may introduce data races
    }
    dispatchMain()
}

main()

See also the discussion on your question in the Swift forums.