I'm trying to make something like python's asyncio.Semaphore
in Swift.
An object like this is used to limit the number of async tasks that that are concurrently accessing a particular resource.
Here's something I got that works, but I'm not happy with it:
class AsyncSemaphore {
var value : Int
class Waiter {
var continuation: CheckedContinuation<Void, Never>
var next : Waiter?
init(continuation: CheckedContinuation<Void, Never>, next: Waiter?) {
self.continuation = continuation
self.next = next
}
}
var waitList : Waiter?
init(value: Int) {
self.value = value
}
func wait() async {
if self.value > 0 {
self.value -= 1
return
}
let _ : Void = await withCheckedContinuation({ k in
let w = Waiter(continuation: k, next:self.waitList)
self.waitList = w
})
self.value -= 1
}
func signal() {
self.value += 1
if let w = self.waitList {
self.waitList = w.next
w.continuation.resume()
}
}
}
actor Actor {
var limit = AsyncSemaphore(value: 2)
func act(_ s : String) async {
await limit.wait()
defer { limit.signal() }
try? await Task.sleep(nanoseconds: 1_000_000_000)
print(s)
}
}
@main
struct Main {
static func main() async {
let a = Actor()
await withTaskGroup(of: Void.self) { g in
for s in ["foo", "bar", "baz", "quux", "fizz", "buzz", "boof", "baaf"] {
g.addTask {
await a.act(s)
}
}
}
}
}
But I wanted it to be a struct:
struct AsyncSemaphore {
var value : Int
class Waiter {
var continuation: CheckedContinuation<Void, Never>
var next : Waiter?
init(continuation: CheckedContinuation<Void, Never>, next: Waiter?) {
self.continuation = continuation
self.next = next
}
}
var waitList : Waiter?
init(value: Int) {
self.value = value
}
mutating func wait() async {
if self.value > 0 {
self.value -= 1
return
}
let _ : Void = await withCheckedContinuation({ k in
let w = Waiter(continuation: k, next:self.waitList)
self.waitList = w
})
self.value -= 1
}
mutating func signal() {
self.value += 1
if let w = self.waitList {
self.waitList = w.next
w.continuation.resume()
}
}
}
actor Actor {
var limit = AsyncSemaphore(value: 2)
func act(_ s : String) async {
await limit.wait() // ERROR on this line
defer { limit.signal() }
try? await Task.sleep(nanoseconds: 1_000_000_000)
print(s)
}
}
@main
struct Main {
static func main() async {
let a = Actor()
await withTaskGroup(of: Void.self) { g in
for s in ["foo", "bar", "baz", "quux", "fizz", "buzz", "boof", "baaf"] {
g.addTask {
await a.act(s)
}
}
}
}
}
But that gives me the error "Cannot call mutating async function 'wait()' on actor-isolated property 'limit'".
So I guess I have a few questions.
Why is this an error? Yes,
limit
is of a value type, but it belongs to Actor, why shouldn't Actor be allowed to mutate it -- in an async method or not?Is there some way to override this error and force swift to let me do it anyway?
Is there a more idiomatic way to to this whole thing?
edit:
This question was based on some faulty premises. Making a semaphore be a struct doesn't really make sense. see comments below