Semaphore alternatives for structured concurrency

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.

1 Like

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()
        }
    }
}
5 Likes

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! :joy:

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.

1 Like

When we use structure concurrency, we should use the actors approach to protect access to attributes and scopes. Then, if we want actors 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 actors 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+

6 Likes