I have a function sensMessage in my iOS Project, the function does some logic and then sends a network call, I want to make it work like a queue so that when the completion handler is called another send message will start, this is the code I used:
private let serialQueue = DispatchQueue(label: "com.yourapp.messageQueue")
let sessionManager = Session(configuration:configuration, rootQueue: serialQueue, interceptor:self)
func sensMessage(message:Message, completion: @escaping (Result) -> Void) {
serialQueue.async { [weak self] in
guard let self = self else { return }
print("Start async queue method: \(message.objectID)")
//SOME LOGIC
self.sessionManager.request()
.response(queue: self. serialQueue) { (response) in {
print("Finish async queue method: \(message.objectId)")
completion(.success)
}
}
}
then I called this function multiple times and get this log:
Start async queue method: 43214321431
Start async queue method: 43213331431
Start async queue method: 54354321431
Start async queue method: 54354654651
Finish async queue method: 43214321431
Finish async queue method: 43213331431
Finish async queue method: 54354321431
Finish async queue method: 54354654651
Any idea what is the problem? how i can fix it?
Serial dispatch queues are well suited for managing dependencies between a series of synchronous work items, automatically running them sequentially. But GCD is not well suited for a series of asynchronous work items. And, unfortunately, this is precisely your scenario.
A serial dispatch queue will launch your asynchronous network requests sequentially, but it will not wait for any of those network requests to finish asynchronous before launching the next one. The network requests will end up running in concurrently (which is what you are trying to avoid).
The legacy solution for a series of asynchronous tasks would be an
OperationQueue, where you can wrap the asynchronous task in a customOperationsubclass. But this subclass will have to perform KVO notifications forisExecuting,isFinished, etc., in order to let the operation queue know when one operation finishes and the next one can start. It is also an unnecessarily complicated process. But, if you are interested, see Trying to Understand Asynchronous Operation Subclass.Sometimes folks would reach for third-party solutions (promises/futures). Or, Combine can handle this well, too. See How To Download Multiple Files Sequentially using NSURLSession downloadTask in Swift for an example of how to constrain concurrency via a number of these technologies. That’s focusing on downloads, but the same basic idea works for posting data, too. And Alamofire has built-in Combine publishers, if that’s the way you wanted to go.
That having been said, the contemporary approach is Swift concurrency, which is written to greatly simply dependencies between tasks that are, themselves, asynchronous. See WWDC 2021’s Meet async/await in Swift, as well as the other videos on that page. And, fortunately, your example is using Alamofire, which has built-in
async-awaitrenditions of their API, too.So, personally, I would use Alamofire’s
async-awaitrenditions of their API (scrupulous avoiding any completion handler based API), and then have afor-await-inloop, iterating through anAsyncChannel<Message>which would send the messages.E.g., here is a MRE of that pattern:
Don’t get lost in the details there, but focus on the big picture: When the app starts, I start monitoring the
AsyncChannelfor new messages to send; and when I want to send a message, I justsendit to that channel, and it will send them out sequentially.For the sake a completeness, the other solution would be to just set the
httpMaximumConnectionsPerHostto1, and that would prevent the underlyingURLSessionused by Alamofire from sending more than one request at a time. That is admittedly a fairly narrow solution (limited to just serial network requests to a particular host), where as the above are the more general purpose “how do I perform a series of asynchronous tasks sequentially”, but might be sufficient for your purposes.