Imagine you have some code outside of a Swift Concurrency (async/await) producing values that you need to consume inside an async/await Task in the same order that they are created.
Let’s see a simple example where we pass integers in a loop from a synchronous function to an async function:
func sendValues() {
for i in 0..<1000 {
Task {
await consume(i)
}
}
}
func consume(_ value: Int) async {
print(value) // values received NOT in order
}
/*
Output:
1
4
0
8
10
9
3
12
13
14
8
...
*/
You would expect to see values printed in order: 1… 2… 3… etc., but it is not the case here.
The sentence above is no longer true in the official Swift 6 implementation, thanks to: SE-0431
Unstructured tasks created inside the loop are scheduled on the global concurrent executor, which means they can be executed in any order:
Task {
await consume(i)
}
What about @MainActor? Main actor’s work is executed on main queue which is serial:
Task { @MainActor in
await consume(value)
}
It won’t work either because the task will be scheduled on a global executor, then execute its closure, realize it’s MainActor closure, and only then ‘hop’ to MainActor.
Apple engineers are aware of those limitations and said that they are working on them.
Are there any remaining options? Yes – AsyncStream:
func sendValues() {
// Create async stream
var continuation: AsyncStream.Continuation!
self.stream = AsyncStream.init {
continuation = $0
}
// Send values 'in order' to async stream
for i in 0..<1000 {
continuation.yield(i)
}
continuation.finish()
// Consume values
consume(stream: stream)
}
func consume(stream: AsyncStream) {
Task {
for await value in stream {
print(value) // values received in order
}
}
}