I have an application that communicates with custom external hardware on the network (using UDP).
I have a thread that receives and process the UDP data and then signals a waiting thread by releasing a semaphore when data is available. A have a asyncSendAndReceive and asyncReceive function that just begs to use async/await.
But I cannot simply switch because of the use of the semaphore. Various forums and discussions said that semaphores should no longer be used for signalling. If not semaphores, then what else?
Note that my two async functions may not always block. If data was received before they were called, then it is queued (and the semaphore is signalled).
So, regardless of Swift concurrency I think it’d be worthwhile switching to Network framework. It has a much nicer API for dealing with this sort of thing. Specifically, Network framework deals with flow control implicitly, based on the way that you call its receive routines.
The standard pattern looks like this:
func receive(from connection: NWConnection) {
connection.receiveMessage { content, context, isComplete, error in
… process the message …
if … we’re done … {
return
}
receive(from: connection)
}
}
Note that the ‘recursive’ call to receive(from:) does not have to be done from the callback. The callback can, if you want, asynchronously process the message and then call receive(from:) when you’re done.
If it takes you a long time to process the message, some messages will back up within the connection but then NWConnection will start to assert flow control on the ‘wire’ [1].
One key element of Swift concurrency is the withCheckedContinuation(function:_:) routine [2]. This lets you convert classic completion handler routines into Swift async functions. For example, imagine you have a completion handler routine like this:
func varnish(waffle: Waffle, completionHandler: @escaping (Result<SurfaceFinish, Error>) -> Void) {
…
}
You can bridge that into Swift concurrency using this:
func varnish(waffle: Waffle) async throws -> SurfaceFinish {
try await withCheckedThrowingContinuation { continuation in
varnish(waffle: waffle) { result in
switch result {
case .failure(let error):
continuation.resume(throwing: error)
case .success(let finish):
continuation.resume(returning: finish)
}
}
}
}
So, it might seem that it’s easy to join these two dots. Alas, it’s not that easy. The issue is cancellation. These continuation-based bridges have to support cancellation and that’s makes things quite a bit more challenging. I don’t have a ready solution to that problem. The basic building block is the withTaskCancellationHandler(operation:onCancel:) routine. The key challenge is that the onCancel closure can be called from any context, and so you need some sort of basic concurrency primitive, like a lock, to protect the data structures you use to track cancellation.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] For UDP this just means it drops messages. For TCP, the window closes and that causes the sender to stop sending.
[2] And it’s friends. In my example I actually used the throwing variant, withCheckedThrowingContinuation(function:_:).