dannys42/SwiftAsyncSerialQueue
A simple async serial queue for Swift concurrency
Example
Simple Example
let serialQueue = AsyncSerialQueue()
func example() {
serialQueue.async {
print("1")
}
serialQueue.async {
print("2")
}
serialQueue.async {
print("3")
}
print("apple")
}Note that example() does not need to be declared async here.
The numbers will always be output in order:
1
2
3However, there is no guarantee where apple may appear:
apple
1
2
3or
1
2
apple
3or any other combination
Waiting for the queue
There may be some cases (e.g. unit tests) where you need to wait for the serial queue to be empty.
func example() async {
serialQueue.async {
print("1")
}
await serialQueue.wait()
serialQueue.async {
print("2")
}
}example() will not complete return 1 is printed. However, it could return before 2 is output.
Mixing sync and async - Barrier Blocks
Similarly, if looking for something similar to barrier blocks:
func example() async {
serialQueue.async {
print("1")
}
await serialQueue.sync {
print("2")
}
serialQueue.async {
print("3")
}
print("apple"")
}In this case, apple will never appear before 2. And example() will not return until 2 is printed.
AsyncCoalescingQueue
Coalescing Queues can be a useful technique especially in flows where you only care about the first and last event, but would like to drop interim events if processing is still in play. For example when processing user input, perhaps you want the first event in order to kick off processing and provide user immediate feedback, and you also want the last event because that represents the most up-to-date user state requested. For example, consider a scrubber for an audio player.
In the GCD approach, coalescing queues acted on a trigger but could not take input very easily. In this Swift implementation, the API is kept simply by relying on scoped variables at the point of call. However, if there are multiple .run() blocks, make sure that it is acceptable for any of them to get dropped. If multiple .run() blocks are required, consider placing the common code in a separate function that is called within the multiple .run() blocks. Also consider whether multiple AsyncCoalescingQueue() instances or AsyncSerialQueue() may be a better fit.
AsyncCoalescingQueue is somewhat similar to debounce and throttle in Combine. debounce has a fixed lag before the first event is emitted, requiring an additional Concatenate publisher if you do not wish to miss the first event. throttle works on fixed time intervals, but may require tuning to balance the task with the speed of the hardware. In some UI cases, you may wish to provide "best effort" responsiveness to the user. AsyncCoalescingQueue (like GCD coalescing queues) will ensure responsiveness by automatically scaling to the workload and hardware it is running on.
Example
The following code:
let coalescingQueue = AsyncCoalescingQueue()
coalescingQueue.run {
try? await Task.sleep(for: .seconds(5))
print("Run 1")
}
coalescingQueue.run {
try? await Task.sleep(for: .seconds(5))
print("Run 2")
}
coalescingQueue.run {
try? await Task.sleep(for: .seconds(5))
print("Run 3")
}
coalescingQueue.run {
try? await Task.sleep(for: .seconds(5))
print("Run 4")
}
coalescingQueue.run {
try? await Task.sleep(for: .seconds(5))
print("Run 5")
}Will output the following:
Run 1
Run 5And take 10 seconds to complete executing.
Alternatives
Some related libraries:
References
Package Metadata
Repository: dannys42/SwiftAsyncSerialQueue
Homepage: https://swiftpackageindex.com/dannys42/SwiftAsyncSerialQueue/main/documentation/asyncserialqueue
Stars: 11
Forks: 0
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
Topics: async, await, concurrency, queue, serial, swift
README: README.md