We hit an intermittent crash in production using chunks(ofCount:or:) on an AsyncStream. It comes down to the os_unfair_lock inside MergeStorage being locked on one cooperative-pool thread and then unlocked on another, which os_unfair_lock doesn't allow:
EXC_BREAKPOINT
_os_unfair_lock_unowned_abort
chunks(ofCount:or:) builds a merge of the stream and an AsyncTimerSequence under the hood, so this is really a merge issue. It looks like the same root cause as #341 — merge resumes its continuation from an unstructured task without carrying the executor, so the resume (and the unlock) lands on a different thread than the one that took the lock.
Stack trace
Thread 48, queue = 'com.apple.root.user-initiated-qos.cooperative'
stop reason = EXC_BREAKPOINT
#0 _os_unfair_lock_unowned_abort libsystem_platform.dylib
#1 _os_unfair_lock_unlock_slow libsystem_platform.dylib
#2 static Lock.unlock(_:) Locking.swift:103
#3 Lock.unlock() Locking.swift:136
#4 closure #2 in closure #1 in MergeStorage.next(continuation:) MergeStorage.swift:109
#7 closure #1 in MergeStorage.next() MergeStorage.swift:107
#9 withTaskCancellationHandler(operation:onCancel:isolation:) libswift_Concurrency
#11 MergeStorage.next() MergeStorage.swift:65
#12 AsyncMerge2Sequence.Iterator.InternalClass.next() AsyncMerge2Sequence.swift:92
#13 AsyncMerge2Sequence.Iterator.next() AsyncMerge2Sequence.swift:103
#14 AsyncChunksOfCountOrSignalSequence.Iterator.next() AsyncChunksOfCountOrSignalSequence.swift:129
#16 AsyncIteratorProtocol.next(isolation:) libswift_Concurrency
#17 for await batch in stream.chunks(ofCount: 64, or: .repeating(every: .seconds(30)))
The lock is taken in MergeStorage.next() (frame #11) and released in the continuation-resume closure (frame #4), which runs on a different thread.
Repro
A producer task yields into an AsyncStream while a consumer drains it through chunks(ofCount:or:). The count-vs-timer merge keeps both branches hot, so the lock acquire and release frequently land on different threads. It's a timing race, so it doesn't fail on the first iteration — running several concurrently makes it show up reliably, usually within seconds to a minute.
import AsyncAlgorithms
func reproduce() async {
let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
let producer = Task.detached {
for i in 0..<1_000_000 {
continuation.yield(i)
}
continuation.finish()
}
for await batch in stream.chunks(ofCount: 64, or: .repeating(every: .milliseconds(10))) {
_ = batch.count
}
await producer.value
}
await withTaskGroup(of: Void.self) { group in
for _ in 0..<50 {
group.addTask { await reproduce() }
}
}
Reproduces faster on multi-core hardware. A shorter timer interval and more concurrent children widen the race window.
Environment
- swift-async-algorithms 1.1.4
- Swift 6.3.3 release toolchain
- iOS (device and simulator), arm64
- Default cooperative executor, no custom executor
We hit an intermittent crash in production using
chunks(ofCount:or:)on anAsyncStream. It comes down to theos_unfair_lockinsideMergeStoragebeing locked on one cooperative-pool thread and then unlocked on another, whichos_unfair_lockdoesn't allow:chunks(ofCount:or:)builds amergeof the stream and anAsyncTimerSequenceunder the hood, so this is really amergeissue. It looks like the same root cause as #341 —mergeresumes its continuation from an unstructured task without carrying the executor, so the resume (and the unlock) lands on a different thread than the one that took the lock.Stack trace
The lock is taken in
MergeStorage.next()(frame #11) and released in the continuation-resume closure (frame #4), which runs on a different thread.Repro
A producer task yields into an
AsyncStreamwhile a consumer drains it throughchunks(ofCount:or:). The count-vs-timer merge keeps both branches hot, so the lock acquire and release frequently land on different threads. It's a timing race, so it doesn't fail on the first iteration — running several concurrently makes it show up reliably, usually within seconds to a minute.Reproduces faster on multi-core hardware. A shorter timer interval and more concurrent children widen the race window.
Environment