I'm trying to onboard with Swift structured concurrency by writing a simple actor to fetch URL responses from a rate-limited web service. I thought that this is something that DispatchSemaphore might be useful for but reading up async-await, I see that DispatchSemaphore should not be used there because of a number of issues (deadlock, etc). Is there some better way of thinking about this problem in Swift Structured Concurrency or is this something that is better done with a DispatchQueue/OperationQueue? I've read Incremental migration to Structured Concurrency which touches on similar issues but didn't come away from that with an actual solution.
I don't have time to type up a full post rn, but see https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAlgorithms.docc/Guides/Throttle.md
Alternatively you could also write your own async-await friendlier semaphore (though this needs to be modified if you want to support cancellation scenarios etc.)
actor Semaphore {
private var count: Int
private var waiters: [CheckedContinuation<Void, Never>] = []
init(count: Int = 0) {
self.count = count
}
func wait() async {
count -= 1
if count >= 0 { return }
await withCheckedContinuation {
waiters.append($0)
}
}
func release(count: Int = 1) {
assert(count >= 1)
self.count += count
for _ in 0..<count {
if waiters.isEmpty { return }
waiters.removeFirst().resume()
}
}
}
Thank you for the semaphore example.
I am already using it (replaced release
by signal
) in a process pipeline and my channel actor no longer deadlocks!
Next, I am going to use it in a fan-in fan-out configuration to see how it goes.
Oddly enough, I am working on such synchronization scenarios between multiple task contexts in this repo.
While ago, I implemented an async semaphore with cancellation scenarios support, you can look into it for more details. The documentation for this I am already hosting on GitHub pages that should be helpful as well. To get some sample usage ideas you can look into the tests for this as well.
When we use structure concurrency, we should use the actor
s approach to protect access to attributes and scopes. Then, if we want actor
s to be able to manage concurrency in part of their scope's function we should extract those scopes from the actor
and await
on them when they are called inside actor
s functions.
I can not think about an example with DispatchSemaphore that we can not migrate to structured concurrency with the need for semaphores. And I will be more than happy to work on any example that you want to share in here.
I have published groue/Semaphore, a micro-library that contains a Swift-concurrency-friendly counting semaphore.
Features:
-
No blocked threads, only suspended tasks.
-
Opt-in support for cancellation:
// Ignores Task cancellation await semaphore.wait() // Throws CancellationError if task is cancelled try await semaphore.waitUnlessCancelled()
-
Tested
-
No dependency, one-file library.
-
iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+
Works like magic. Thanks!