Is the order of Task execution in this code deterministic?

Hi, I'd like to confirm whether the order of Task execution in the below code is deterministic or not.

Environment:
Xcode14.0(14A309)
Device: iPhone SE3
OSVer: iOS16.1(20B5045d)

@MainActor
func runTest() async {
    print("1")
    await MainActor.run {
        print("2")
        Task { @MainActor in
            print("3")
        }
        print("4")
    }
    print("5")
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            await runTest()
        }
    }
}

The result is always 1,2,4,3,5.

I also confirmed it on a simulator(iOS16.0) and Swift playground.

I'm not sure whether 3 and 5 is always the same order or not.

Is it deterministic? or does it just depend on implementation detail?

1 Like

I don't know that it's clearly documented when the Task is queued onto the main actor relative to the code around it, but it's hard to imagine any implementation that would not print "3" before "5". Still, I don't think this a great way to write this code, and I would suggest that using MainActor.run is a "code smell" here.

Since runTest is isolated to the main actor, there's really no point in using run that I can see from this example. The prints are necessarily going to run sequentially, other than perhaps "3", so you may as well write:

@MainActor
func runTest() async {
    print("1")
    print("2")
    Task { @MainActor in
        print("3")
    }
    print("4")
    print("5")
}

Now it becomes a question of why you are interested in an ordering between "3" and "5". If you want all of the task "3" to finish before "5", then you could just as well rewrite this as:

@MainActor
func runTest() async {
    print("1")
    print("2")
    print("4")
    print("3")
    print("5")
}

Note also that there's no real need for runTest to be async in either of these rewritten versions, and if you drop the keyword there's no await in viewDidLoad either:

@MainActor
func runTest() {
    print("1")
    print("2")
    print("4")
    print("3")
    print("5")
}
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
           runTest()
        }
    }
}

It seems to me that the only reason for something more complicated would be if it's somehow wrong in your scenario to fix the order of execution.

2 Likes

Thanks for your kind answer.

when the Task is queued onto the main actor relative to the code around it

This is what I want to know.

I don't think this a great way to write this code, and I would suggest that using MainActor.run is a "code smell" here.

I agree that this code is not practical and we should not write this kind of code if it's not deterministic.

This code is just an experiment to learn how Task is queued onto the main actor.

Thanks.

1 Like

i posed a similar question recently, but with slightly different specifics. my takeaway from the resulting commentary was that treating Task { @MainActor in ... } as analogous to enqueuing work on a FIFO serial queue is subtly inaccurate. i'm not sure there's any guarantee in the concurrency model that the print("3") and print("5") statements will be ordered in a particular way.

The safest assumption about any "is there a guarantee around the order of execution between tasks" question is "no". The purpose of a task is to allow concurrency; within the task, you get sequential execution, but the task naturally wants to run independently of other tasks, and you should make minimal assumptions about how it executes with respect to other tasks, and use explicit communication mechanisms between tasks, such as task groups, async let, or the channels and queues from swift-async-algorithms, where you need communication. If you need a sequence of things to execute in a specific order, the best way to go about that is to have those things happen in that specific order on a single task.

11 Likes

I see. I appreciate for your answer!

Note though that from my experience a task iterating on an async sequence does not guarantee fully sequential behavior if the body of the loop has async functions. It appears to run parts of the body for one element interlieved with later elements. Placing an NSLock forced sequential behavior, but in swift 6 that is not going to work.

Do you have an example? Something is seriously wrong if code within a task is not executing sequentially.