-
Notifications
You must be signed in to change notification settings - Fork 170
Description
While testing AsyncChannel
behavior to determine if it was a good match for my use case, I noticed that when AsyncChannel.send(_:)
is called from different Task
s, the order of the output is inconsistent and unpredictable, even if the schedule happens on a specific actor and so does the reading.
The following script sends 500 elements, one after the other, each in its own Task
. This recreates a common scenario of the real word where new elements generates from user inputs in a synchronous context and the call to send(_:)
has to be wrapped in a Task to bridge sync and async worlds. This behavior is also described at [Pitch] Async buffered channel - Related Projects / Swift Async Algorithms - Swift Forums.
import Foundation
import AsyncAlgorithms
@globalActor actor ConsumerActor: GlobalActor {
static let shared = ConsumerActor()
}
func testAsyncChannel() async throws {
let channel = AsyncChannel<Int>()
let task1 = Task { @ConsumerActor in
var count = 0
for await value in channel {
count += 1
if value != count {
print(value, "on ConsumerActor does not match", count)
}
// print("AsyncChannel value: ", value)
}
}
for element in 1...500 {
Task { @MainActor in
// print("Sending \(element)")
await channel.send(element)
}
// try await Task.sleep(for: .nanoseconds(30000)) // For values above 30000 ns, the order is respected.
}
}
try await testAsyncChannel()
try await Task.sleep(for: .seconds(2))
The output this script is:
3 on ConsumerActor does not match 1
4 on ConsumerActor does not match 2
5 on ConsumerActor does not match 3
6 on ConsumerActor does not match 4
7 on ConsumerActor does not match 5
1 on ConsumerActor does not match 6
8 on ConsumerActor does not match 7
12 on ConsumerActor does not match 8
13 on ConsumerActor does not match 9
[...]
Showing that the order the elements are emitted by the AsyncChannel
does not correspond to the ordinal numbers.
The expectation is that the elements emitted by the AsyncChannel
are in the same order AsyncChannel.send(_:)
has been called. This happens when calls to AsyncChannel.send(_:)
are at least 30000 ns.
In fact, the following script demonstrates that normally the order of execution of several scheduled Task
is respected when they are scheduled to run on an actor - while the order cannot be determined when an actor is not specified.
import Foundation
actor Counter {
var count = 0
func increment() -> Int {
count += 1
return count
}
}
func experiment() async {
let s = Counter()
for i in 1 ... 1_000_000 {
Task { @MainActor [i] in
let result = await s.increment()
if i != result {
print(i, "on MainActor does not match", result)
}
}
}
let t = Counter()
for i in 1 ... 1_000 {
Task { [i] in
let result = await t.increment()
if i != result {
print(i, "does not match", result)
}
}
}
try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
let finalCountS = await s.count
print("Completed all tasks S, count:", finalCountS)
let finalCountT = await t.count
print("Completed all task T, count:", finalCountT)
}
await experiment()
The output of this script is:
14 does not match 13
40 does not match 39
43 does not match 42
44 does not match 43
[...]
979 does not match 980
995 does not match 994
994 does not match 995
Completed all tasks S, count: 1000000
Completed all task T, count: 1000
Demonstrating that all the 1000000 Task
scheduled on the Main Actor runs in the order they are scheduled.