Passing values to an Task/Actor in serial manner

There's some confused/confusing wording being used in this thread. The Swift book should definitely be improved on concurrency topics such as these, but meanwhile:

Yes, that's the root of the problem. Task{} is scheduled on the global pool, begins running the closure, notices it is @MainActor and hops to it. This is arguably not great, because that is exactly why we lose ordering in such code:

Task { @MainActor in a() }
Task { @MainActor in b() }

which may be "arrive" at the main actor in any order... The "SerialExecutor" protocol means that the task once it is run on such actor executes in the expected serial fashion as you'd expect a task to be executing. The actor doesn't really to much here in terms of ordering.

Yes and no... The only correct way to do this today is very verbose: you have to make an AsyncStream, make a single Task{ for await message in stream {} } to consume these messages, and stream.yield(.message) into it from the outside world. This will produce the expected order: stream.yield(a); stream.yield(b).

It's not great to have to be so verbose about it, and the primary reason Task{} can't do this is because it doesn't know at enqueue time where the code would end up executing. As far as the runtime is concerned it was just passed "some async closure", and we don't have a way to check at runtime "hey, is this actually specifically going to immediately jump to some actor?". We're missing an ability to express and check such thing in the language/compiler.

I'd personally slot this as something we should improve upon in future releases but no plans have been made about this yet. Even with such "better" Task{} you would not be guaranteed order because priority escalation on the returned task could boost it in front of the queue.

One alternative idea would be to introduce a send actor.thing() operation, prototyped here, that would be guaranteed to do the right thing. But both more discussion and Swift evolution are needed to figure out if this is the right solution or if something else might be.


I also see this thread getting a bit confused in wording and terminology.

Manolo's phrase that "Tasks are for executing things serially / in-order" is correct. That is the only way in today's concurrency model to guarantee strict order. Thus why this "consume the messages stream from one task" approach above would work.

Wether or not a task is executed on an actor or just the global pool does not matter at all to be honest. The only thing that guarantees order is how code is executed "step by step" in a Task. It also does not matter for purposes of ordering if a task is structured or unstructured.

Hope this helps

10 Likes