0

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?

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
YosiFZ
  • 7,792
  • 21
  • 114
  • 221
  • Your current problem is that all dispatch global are queued and execute when the system decide to run global queue. The sensMessage finished before the global queue starts. – Ptit Xav Aug 15 '23 at 08:28
  • Try using NSOperationQueue to have other synchronisation possibility. – Ptit Xav Aug 15 '23 at 08:45
  • @PtitXav i update my code with the right network call – YosiFZ Aug 15 '23 at 08:54
  • Your `request` function is, itself, asynchronous. This means that it is queued and then the closure you submitted to your dispatch queue exits, allowing the next submitted task to start – Paulw11 Aug 15 '23 at 08:58

1 Answers1

0

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 custom Operation subclass. But this subclass will have to perform KVO notifications for isExecuting, 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-await renditions of their API, too.

So, personally, I would use Alamofire’s async-await renditions of their API (scrupulous avoiding any completion handler based API), and then have a for-await-in loop, iterating through an AsyncChannel<Message> which would send the messages.


E.g., here is a MRE of that pattern:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    @State var count = 0

    var body: some View {
        VStack {
            Button("Send") {
                Task {
                    let message = Message(objectID: count, message: "Message \(count)")
                    count += 1

                    await viewModel.submit(message)
                }
            }
        }
        .padding()
        .task {
            await viewModel.monitorChannel()
        }
    }
}

@MainActor
final class ViewModel: ObservableObject {
    private let sessionManager = Session()
    private let channel = AsyncChannel<Message>()
}

// MARK: - Public interface

extension ViewModel {
    /// Start monitoring for messages added to channel

    func monitorChannel() async {
        for await message in channel {
            do {
                let result = try await send(message)
                print(result)
            } catch {
                print(error)
            }
        }
    }

    /// Submit a message to be posted to server.
    ///
    /// Client calls this when it wants to enqueue a message to be sent.
    ///
    /// - Parameter message: The message to be posted.

    func submit(_ message: Message) async {
        await channel.send(message)
    }
}

// MARK: - Private implementation details

private extension ViewModel {
    func send(_ message: Message) async throws -> ServerResponse {
        try await sessionManager
            .request(
                "https://httpbin.org/post",
                method: .post,
                parameters: message,
                encoder: JSONParameterEncoder.default
            )
            .validate()
            .serializingDecodable(ServerResponse.self)
            .value
    }
}

struct Message: Codable {
    let objectID: Int
    let message: String
}

struct ServerResponse: Decodable {
    let json: Message
}

Don’t get lost in the details there, but focus on the big picture: When the app starts, I start monitoring the AsyncChannel for new messages to send; and when I want to send a message, I just send it to that channel, and it will send them out sequentially.


For the sake a completeness, the other solution would be to just set the httpMaximumConnectionsPerHost to 1, and that would prevent the underlying URLSession used 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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044