apple / apple/swift-async-algorithms
FIFO not respected in `AsyncChannel`
- Dominant language
- Swift
- Stars
- 3.7k
- Forks
- 226
- Avg merge
- 10d 3h
- Merged PRs (30d)
- 1
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](https://forums.swift.org/t/pitch-async-buffered-channel/59854).
```swift
import Foundation
import AsyncAlgorithms
@globalActor actor ConsumerActor: GlobalActor {
static let shared = ConsumerActor()
}
func testAsyncChannel() async throws {
let channel = AsyncChannel()
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:
```plain
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.
```swift
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:
```plain
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.
Contributor guide
Research direction
Start by running the reported AsyncChannel reproduction that creates 500 Tasks calling send(_:) and compare emitted values with their input order. Read the AsyncChannel send and iteration behavior, then identify how ordering is defined when sends originate from separate Tasks. Done means the expected FIFO behavior is specified and covered by a regression test, or the issue is clarified if that guarantee is not intended.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100