Hello,
I am living in a world of joy with actors and my async code, it has made race prone code reliable, I no longer have to debug odd corruption and hangs, and my code is a lot cleaner as a result - but I am facing an ugly challenge.
I have been using an idiom that is frowned upon in Swift (See Swift Concurrency: Behind the Scenes):
let semaphore = DispatchSemaphore (value: 0)
Task {
await something ()
semaphore.signal()
}
semaphore.wait ()
Not only it frowned upon, but I have now experienced in my own flesh the deadlock, and I am at a loss as to what to do about it. The talk does not really offer a solution to this problem, and this year I thought I had seen a ray of light in the talk "Visualize and Optimize Swift Concurrency".
Now, I know what you are thinking "Miguel, just go and rewrite that in a different way, make your entire pipeline truly asynchronous". Let me explain why I have not been able to do this.
The above code is intended to fulfill a contract from a C API, where the C code invokes a callback to send some data over a connection. The contract expects the method to execute and return the number of bytes written. This is invoked implicitly via various APIs, as part of an existing actor.
So my implementation ends up looking like this:
// called within my first actor context
func send_callback (buffer: Data) -> Int {
let semaphore = DispatchSemaphore (value: 0)
var status: Int
Task {
// transport is a separate actor
status = await transport.send (buffer)
semaphore.signal()
}
semaphore.wait ()
return status
}
Here transport
is my actor that ensures my serial access. This, sadly, deadlocks, and it is invoked deeply into the code of an actor.
The new Xcode at least was nice enough to tell me that there was some priority inversion going on here, where a higher priority task was waiting on a lower priority one. I tried things like using Task (priority: high)
and Task.detached
, neither one helps me here.
The ray of light from the talk this year on visualizing concurrency, suggested that maybe I could get lucky if I used a DispatchQueue, so I created a DispatchQueue for this purpose, and then added a task inside (because my transport is an actor, and that is the only way I have found to call into it):
func send_callback (buffer: Data) -> Int {
let semaphore = DispatchSemaphore (value: 0)
var status: Int
myQueue.async {
Task {
status = await transport.send (buffer)
semaphore.signal()
}
}
semaphore.wait ()
return status
}
I am not married to my semaphore, but I can not figure out any other way to wait for the task to complete. Calling Task.value
requires an async context, which I do not have.