Using Swift's new async/await functionality, I want to emulate the scheduling behavior of a serial queue (similar to how one might use a DispatchQueue or OperationQueue in the past).
Simplifying my use case a bit, I have a series of async tasks I want to fire off from a call-site and get a callback when they complete but by design I want to execute only one task at a time (each task depends on the previous task completing).
Today this is implemented via placing Operations onto an OperationQueue with a maxConcurrentOperationCount = 1, as well as using the dependency functionality of Operation when appropriate. I've build an async/await wrapper around the existing closure-based entry points using await withCheckedContinuation but I'm trying to figure out how to migrate this entire approach to the new system.
Is that possible? Does it even make sense or am I fundamentally going against the intent of the new async/await concurrency system?
I've dug some into using Actors but as far as I can tell there's no way to truly force/expect serial execution with that approach.
--
More context - This is contained within a networking library where each Operation today is for a new request. The Operation does some request pre-processing (think authentication / token refreshing if applicable), then fires off the request and moves on to the next Operation, thus avoiding duplicate authentication pre-processing when it is not required. Each Operation doesn't technically know that it depends on prior operations but the OperationQueue's scheduling enforces the serial execution.
Adding sample code below:
// Old entry point
func execute(request: CustomRequestType, completion: ((Result<CustomResponseType, Error>) -> Void)? = nil) {
let operation = BlockOperation() {
// do preprocessing and ultimately generate a URLRequest
// We have a URLSession instance reference in this context called session
let dataTask = session.dataTask(with: urlRequest) { data, urlResponse, error in
completion?(/* Call to a function which processes the response and creates the Result type */)
dataTask.resume()
}
// queue is an OperationQueue with maxConcurrentOperationCount = 1 defined elsewhere
queue.addOperation(operation)
}
// New entry point which currently just wraps the old entry point
func execute(request: CustomRequestType) async -> Result<CustomResponseType, Error> {
await withCheckedContinuation { continuation in
execute(request: request) { (result: Result<CustomResponseType, Error>) in
continuation.resume(returning: result)
}
}
}
A few observations:
For the sake of clarity, your operation queue implementation does not “[enforce] the serial execution” of the network requests. Your operations are only wrapping the preparation of those requests, but not the performance of those requests (i.e. the operation completes immediately, and does not waiting for the request to finish). So, for example, if your authentication is one network request and the second request requires that to finish before proceeding, this
BlockOperationsort of implementation is not the right solution.Generally, if using operation queues to manage network requests, you would wrap the whole network request and response in a custom, asynchronous
Operationsubclass (and not aBlockOperation), at which point you can use operation queue dependencies and/ormaxConcurrentOperationCount. See https://stackoverflow.com/a/57247869/1271826 if you want to see what aOperationsubclass wrapping a network request looks like. But it is moot, as you should probably just useasync-awaitnowadays.You said:
No. Actors can ensure sequential execution of synchronous methods (those without
awaitcalls, and in those cases, you would not want anasyncqualifier on the method itself).But if your method is truly asynchronous, then, no, the actor will not ensure sequential execution. Actors are designed for reentrancy. See SE-0306 - Actors » Actor reentrancy.
If you want subsequent network requests to await the completion of the authentication request, you could save the
Taskof the authentication request. Then subsequent requests couldawaitthat task:If you are looking for general queue-like behavior, you can consider an
AsyncChannel. For example, in https://stackoverflow.com/a/75730483/1271826, I create aAsyncChannelfor URLs, write a loop that iterate through that channel, and perform downloads for each. Then, as I want to start a new download, Isenda new URL to the channel.Perhaps this is unrelated, but if you are introducing async-await, I would advise against
withCheckedContinuation. Obviously, if iOS 15 (or macOS 12) and later, I would use the new asyncURLSessionmethods. If you need to go back to iOS 13, for example, I would usewithTaskCancellationHandlerandwithThrowingCheckedContinuation. See https://stackoverflow.com/a/70416311/1271826.