swiftgrand-central-dispatchdispatchdispatch-asyncdispatch-queue

How can i wait to receive a response from a DispatchWorkItem before moving on to the next request or next DispatchWorkItem in a Dispatch Queue


I have an array of dispatch workItems, how to wait until one work is completed before i move on to the next work in the queue?

func AsyncCalls(statusHandler: @escaping (String) -> Void){

    var dispatchWorkItems : [DispatchWorkItem] = []
    let categoryWorkItem = DispatchWorkItem {
        main {
        return  statusHandler("Loading categories ")
        }
        self.modelView.getCategories(completion: { data,error in
            main {
            if data.isEmpty {
            return  statusHandler("\(error )")
            }else{
            return  statusHandler("Done loading categories")
            }
            }
        })
    }

    let itemsWorkItem = DispatchWorkItem {
        main {
            return statusHandler("Loading Inventory ")
        }
        self.modelView.getInventory(completion: { data,error in
            main {
                if data.isEmpty {
                return  statusHandler("\(error )")
                }else{
                return  statusHandler("Done loading Inventory")
                }
            }
        })
    }


    dispatchWorkItems.append(categoryWorkItem)
    dispatchWorkItems.append(itemsWorkItem)



    let queue = DispatchQueue(label: "com.dataLoader")
    let group = DispatchGroup()

    dispatchWorkItems.forEach{queue.async(group: group, execute: $0)}

    group.notify(queue: .main) {
        main{

        }
    }
}

How can i simplify the above method or how do i apply semaphores or any other accepted approach to help me wait until i get a response from a DispatchworkItem before moving on to execute the next DispatchworkItem in a queue

A modelView to getData from server looks something like the below

    func getInventory(completion: @escaping ArrayClosure<[InventoryClass], String>){
    let parameters :  [(String,AnyObject)] = [

        ("PageNumber" ,  "1" as AnyObject),
        ("Limit","1000" as AnyObject),
        ("BranchIds","\(business.branch?.id ?? "")" as AnyObject),
        ("canBeSold","true" as AnyObject)

    ]

    InventoryService(authorizationHeader:  self.header).getInventory(parameters: parameters) { request in
        switch request {

        case .success(let data):
            guard let finalData = data.data else  {return completion([], "Request to get Inventory Items was sucessfull but items count is 0")}
            return completion([finalData],"")
        case .failure(let error):
             return completion([],error.localizedDescription)

        }
    }
}

Solution

  • DispatchWorkItem simply is not well suited for managing dependencies between tasks that are, themselves, asynchronous. Theoretically, one can use kludgy techniques such as semaphores to manage dependencies between asynchronous tasks in DispatchWorkItem, but that is an anti-pattern and should be assiduously avoided.

    Historically, the elegant solution to manage dependencies between asynchronous tasks was a custom asynchronous Operation subclass, but as you will see in my original answer below in, it is unnecessarily complicated.

    Nowadays, the go to solution for dependencies between asynchronous tasks is async-await of Swift concurrency. See WWDC 2021 video, Meet async/await in Swift. On that page they have tons of links to other concurrency related videos. Once you grok the basic ideas of async-await, Swift concurrency: Update a sample app is a good practical demonstration of converting a legacy app to Swift concurrency.

    So, for example, with async-await, that the complicated asyncCalls of your question is reduced to something quite trivial:

    func asyncCalls() async throws -> [InventoryClass] {
        let categories = try await service.getCategories()
        return try await service.getInventory(for: categories)
    }
    

    In this code snippet, it won’t call getInventory until getCategories returns. And it achieves this without blocking any threads.

    Now in your example, getCategories doesn’t do anything with its results, but in the above I assumed that you want to make them run sequentially because you need the results from the first request in order to perform the second request. (Otherwise, you should run them concurrently, not sequentially.)

    Needless to say, you now need to convert getCategories and getInventory to use async-await, rather than use completion handlers. We don’t have enough of their implementations in the question to be too specific on that point, but hopefully after watching some of the above videos (especially that one walking you through an update of a sample app), you will better understand how to convert them.


    In my original example below, I outline an example of an Operation subclass for downloading a bunch of assets sequentially. With Swift concurrency, all of that silliness disappears and is reduced to a few lines of code:

    func download(urls: [URL]) async throws {
        for url in urls {
            let (data, response) = try await URLSession.shared.data(from: url)
            // do something with `data` and `response` here
        }
    }
    

    Compare that to the complicated Operation subclass below, and you’ll really appreciate the elegance of Swift concurrency.

    So, while I would no longer recommend the Operation pattern, as outlined in my original answer below, I will include it for historical reference:


    When I want to establish dependencies between asynchronous tasks, I have historically used Operation rather than DispatchWorkItem. (Admittedly, in iOS 13 and later, we might contemplate Combine’s Future/Promise, but for now operations are the way to go.) Operations have been designed to support wrapping of asynchronous processes much more elegantly than DispatchWorkItem. So you can use a queue whose maxConcurrentOperationCount is 1, like so:

    let networkQueue = OperationQueue()
    networkQueue.maxConcurrentOperationCount = 1
    
    let completionOperation = BlockOperation {
        print("all done")
    }
    
    for url in urls {
        let operation = NetworkOperation(url: url) { result in
            switch result {
            case .failure(let error):
                …
    
            case .success(let data):
                …
            }
        }
        completionOperation.addDependency(operation)
        networkQueue.addOperation(operation)
    }
    
    OperationQueue.main.addOperation(completionOperation)
    

    Or you can use a more reasonable maxConcurrentOperationCount and use dependencies only between those operations where you need this sequential behavior:

    let networkQueue = OperationQueue()
    networkQueue.maxConcurrentOperationCount = 4
    
    let completionOperation = BlockOperation {
        print("all done")
    }
    
    var previousOperation: Operation?
    
    for url in urls {
        let operation = NetworkOperation(url: url) { result in
            switch result {
            case .failure(let error):
                …
    
            case .success(let data):
                …
            }
        }
        if let previousOperation {
            operation.addDependency(previousOperation)
        }
        completionOperation.addDependency(operation)
        networkQueue.addOperation(operation)
        previousOperation = operation
    }
    
    OperationQueue.main.addOperation(completionOperation)
    

    This is what that NetworkOperation might look like:

    class NetworkOperation: AsynchronousOperation {
        typealias NetworkCompletion = (Result<Data, Error>) -> Void
        
        enum NetworkError: Error {
            case invalidResponse(Data, URLResponse?)
        }
        
        private var networkCompletion: NetworkCompletion?
        private var task: URLSessionTask!
        
        init(request: URLRequest, completion: @escaping NetworkCompletion) {
            super.init()
            
            task = URLSession.shared.dataTask(with: request) { data, response, error in
                defer {
                    self.networkCompletion = nil
                    self.finish()
                }
                
                guard let data = data, error == nil else {
                    self.networkCompletion?(.failure(error!))
                    return
                }
                
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    200..<300 ~= httpResponse.statusCode
                    else {
                        self.networkCompletion?(.failure(NetworkError.invalidResponse(data, response)))
                        return
                }
                
                self.networkCompletion?(.success(data))
            }
            networkCompletion = completion
        }
        
        convenience init(url: URL, completion: @escaping NetworkCompletion) {
            self.init(request: URLRequest(url: url), completion: completion)
        }
        
        override func main() {
            task.resume()
        }
        
        override func cancel() {
            task.cancel()
        }
    }
    

    This is passing back Data, but you can write permutations/subclasses that further parse that into whatever your web service is returning using JSONDecoder or whatever. But hopefully this illustrates the basic idea.

    The above uses this AsynchronousOperation class:

    /// Asynchronous operation base class
    ///
    /// This is abstract to class performs all of the necessary KVN of `isFinished` and
    /// `isExecuting` for a concurrent `Operation` subclass. You can subclass this and
    /// implement asynchronous operations. All you must do is:
    ///
    /// - override `main()` with the tasks that initiate the asynchronous task;
    ///
    /// - call `completeOperation()` function when the asynchronous task is done;
    ///
    /// - optionally, periodically check `self.cancelled` status, performing any clean-up
    ///   necessary and then ensuring that `finish()` is called; or
    ///   override `cancel` method, calling `super.cancel()` and then cleaning-up
    ///   and ensuring `finish()` is called.
    
    class AsynchronousOperation: Operation {
    
        /// State for this operation.
    
        @objc private enum OperationState: Int {
            case ready
            case executing
            case finished
        }
    
        /// Lock for synchronizing access to `state`.
    
        private let lock = NSLock()
    
        /// Private backing stored property for `state`.
    
        private var _state: OperationState = .ready
    
        /// The state of the operation
    
        @objc private dynamic var state: OperationState {
            get { synchronized { _state } }
            set { synchronized { _state = newValue } }
        }
    
        // MARK: - Various `Operation` properties
    
        open         override var isReady:        Bool { return state == .ready && super.isReady }
        public final override var isAsynchronous: Bool { return true }
        public final override var isExecuting:    Bool { return state == .executing }
        public final override var isFinished:     Bool { return state == .finished }
    
        // KVO for dependent properties
    
        open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
            if [#keyPath(isReady), #keyPath(isFinished), #keyPath(isExecuting)].contains(key) {
                return [#keyPath(state)]
            }
    
            return super.keyPathsForValuesAffectingValue(forKey: key)
        }
    
        // Start
    
        public final override func start() {
            if isCancelled {
                state = .finished
                return
            }
    
            state = .executing
    
            main()
        }
    
        /// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.
    
        open override func main() {
            fatalError("Subclasses must implement `main`.")
        }
    
        /// Call this function to finish an operation that is currently executing
    
        public final func finish() {
            if isExecuting { state = .finished }
        }
        
        private func synchronized<T>(block: () throws -> T) rethrows -> T {
            lock.lock()
            defer { lock.unlock() }
            return try block()
        }
    }
    

    There are lots of ways to write a base AsynchronousOperation, and I don’t want to get lost in the details, but the idea is that we now have an Operation that we can use for any asynchronous process.