Task: Is order of task execution deterministic?

Actors cannot guarantee FIFO execution of submitted tasks, because tasks are free to suspend themselves. If a task suspends itself, another task submitted to the same actor may finish before the suspended task.

Similarly, when submitting tasks to an actor, there is no way for the actor to guarantee any sort of ordering. Specifically, when the submissions come from synchronous code, a throw-away task has to be created to invoke the actor from an async context. Thus, code which looks like it's ordered in time may not be, because the scheduling order of the throw-away tasks is not deterministic.

8 Likes

I feel your title causes confusion into how task order is done is causing confusion that is not needed. May I suggest an edit? Task from non async code is not guaranteed to execute ordered This is because tasks from async code are ordered and maintain the structure. As I explain in a PR here feat!: Use async await instead of dispatchqueue which requires swift5.7 I think by doozMen · Pull Request #76 · sushichop/Puppy · GitHub

You could have a property var finished = false that you can await for no? As the actor is isolated this would wait for all to finish?

Thinks will give your code a go to solve an issue I have with Puppy logger feat!: Use async await instead of dispatchqueue which requires swift5.7 I think #76

This isn't really about structure of "structured concurrency" at all. Here is structured concurrency with "any order is okey":

await withTaskGroup(...) { group in 
  group.addTask { print("A") }
  group.addTask { print("B") }
}

This is well-structured concurrency, and it has no ordering guarantees either.

The problem is just™ that Task{} does not enqueue immediately but goes through the global pool first, at which time we lose the ordering guarantee.

If we were to introduce a send-like operation that cannot be awaited on (Task{}.value can be awaited on), we'd be able to provide such expected ordering semantics; I did a prototype of that here: [PoC][Concurrency] Proof out and simulate "send" semantics by ktoso · Pull Request #62213 · apple/swift · GitHub

5 Likes

Thanks very much for clarifying. I made my comment as the difference between awaiting for tasks as contrast to choosing not to wait for a task but still want it to execute in a specific order, like writing logs to a file one by one. I find your explanation of the problem and need very clear and hope it will git in as it is something that is very common in use indeed.

1 Like

Is there a canonical source of documentation for this (skipping .background items in power-save mode) behavior?

How do we get that from a POC to a pitch?

2 Likes

Yea, here and here

It's not necessarily skipping tho. Documentation says background work processing may be paused and usually is:

On iPhones, discretionary and background operations, including networking, are paused when Low Power Mode is enabled. See React to Low Power Mode on iPhones.

Like performSelector:onThread:withObject:waitUntillDone:?

I haven't had time to look into or test this in detail yet, but per the "What's New in Swift" session from WWDC 2023, it really seems like this might be able to be accomplished via custom actor executors.

As far as I understand the executors only determine how the jobs are enqueued but you still have to await when calling something on the actor. While custom serial executors might be something to explore for certain use cases (like deterministic async tests as done in pointfree's concurrency extras package). So I think that won't help us get something like a send functionality.

Resurrecting this thread. Is there a possibility of this actually being handled in the language? Or is this something we just have to accept?

Personally, apart from in the most trivial of cases, I no longer reach for actors as a synchronisation mechanism precisely due to the uncertainty of Task ordering in cases where it seems like we should be able to guarantee it.

The latest case was similar to OP in that I have a Combine publisher that receives events from a UI at a very high frequency. The type that owns the publisher is an actor so I need to create a Task to handle the published value. I have test cases which will invariably fail around ~20% of the time if I run them 100 times due to the events being published in the expected order, but the Task creation throwing things out of whack. This is even the case if the publisher recieves events on the main thread and the tasks are created on the main actor using the highest priority.

Here's the offending code:

timePublisher
        .removeDuplicates()
        .receive(on: DispatchQueue.main)
        .sink { [weak self] time in
                    Task { @MainActor [weak self] in
                        await self?.handleTimeUpdate(to: time)
                    }
                }
                .store(in: &subscriptions)
        }

I can see from logging that the publisher receives the events in the correct order, but the code inside the Task will not always start in the correct order.

I'm not sure what the correct solution is using Swift Concurrency so I reverted to using a combination of locks and dispatch queues across this entire feature

Yes, enqueueing tasks directly on the actor the closure is isolated to is something that can and should be fixed in the language. The isolated function types future direction of SE-0420 outlines the approach we are likely to take to solve this problem:

A more promising approach would be to allow the isolation to be statically erased but still make it dynamically recoverable by carrying it along in the function value, essentially as an extra value of type (any Actor)? . A function type that supports dynamically recovering the isolation would look something like @isolated () -> () , and it could be used to e.g. dynamically propagate the isolation of a function into something like the Task initializer so that the task can immediately start on the right executor.

9 Likes

@ktoso How can we enforce FIFO semantics through custom actor executors?

Would using a SerialDispatchQueue as a custom executor enforce FIFO semantics?


**actor** LoggerWithCustomExecutor {

// DispatchSerialQueue conforms to SerialExecutor

**let** queue = DispatchSerialQueue(label: "my-custom-executor")

// Shared state

**var** messages = [String]()

// Custom executor for the actor

**nonisolated** **var** unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() }

**func** addMessages() {

dispatchPrecondition(condition: .onQueue(queue))

**self**.messages = messages

}

**func** getMessages() -> [String]? {

dispatchPrecondition(condition: .onQueue(queue))

**return** messages

}

}

It’s not enough. Task {} starts on the global pool where reordering can happen.

With future upcoming proposals when we’re able to directly enqueue on target actor by doing Task { [isolated target] in } we’ll be able to get the missing piece

4 Likes

Any chance we get that with Swift 6.0?

Maybe, as it depends on swift-evolution/proposals/0420-inheritance-of-actor-isolation.md at main · apple/swift-evolution · GitHub and work towards that has been progressing.

Basically the Task initializer needs to "get the target executor out of the closure passed to it" and this way know where to enqueue. Such runtime function exists now https://github.com/apple/swift/pull/71941 so I think things are slowly aligning.

Can't make promises about what release it'd make it in though

3 Likes

Sounds great - I think this is an important missing piece of modern concurrency in Swift.

1 Like